|
|
@@ -1,5 +1,6 @@
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
import { PassThrough } from 'node:stream'
|
|
|
+import { resolve } from 'node:path'
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
|
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
|
|
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
|
@@ -8,7 +9,8 @@ import SandboxProvider from '@deepseek-ai/dsh-sandbox'
|
|
|
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
|
|
import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
|
|
import PtyService, { PtyBackendCleanupError, PtySessionId } from '@deepseek-ai/dsh-pty'
|
|
|
-import { LocalPtyBackend } from '@deepseek-ai/dsh-pty-local'
|
|
|
+import type { PtySendRequest, PtyWaitReason } from '@deepseek-ai/dsh-pty'
|
|
|
+import { LocalPtyBackend, PWSH_PROMPT_SETUP } from '@deepseek-ai/dsh-pty-local'
|
|
|
import * as ptyLocal from '@deepseek-ai/dsh-pty-local'
|
|
|
import type { ResolvedConfig } from '@deepseek-ai/dsh-pty-local/src/config.ts'
|
|
|
import type { LocalPtySession } from '@deepseek-ai/dsh-pty-local/src/session.ts'
|
|
|
@@ -37,7 +39,7 @@ class RecordingSandbox extends SandboxProvider {
|
|
|
|
|
|
function config(): ResolvedConfig {
|
|
|
return {
|
|
|
- backendType: 'shell', shellPath: '/bin/bash', shellArgs: [], rows: 24, cols: 80,
|
|
|
+ backendType: 'shell', shellDialect: 'bash', shellPath: '/bin/bash', shellArgs: [], rows: 24, cols: 80,
|
|
|
scrollbackLines: 10, scrollbackMaxBytes: 100, maxReadBytes: 50,
|
|
|
pollIntervalMs: 10, exactProbeAfterMs: 20, idleSilenceMs: 50, handoffGraceMs: 10, timeoutMs: 100,
|
|
|
disposeGraceMs: 10,
|
|
|
@@ -215,7 +217,7 @@ describe('LocalPtyBackend startup rollback', () => {
|
|
|
expect(initialized).toHaveBeenCalledWith(undefined)
|
|
|
expect((ctx.sandbox as RecordingSandbox).calls).toEqual([{
|
|
|
argv: ['/bin/bash', '-i'],
|
|
|
- policy: { mode: 'workspace-write', sessionId: 'agent', workspaceRoot: '/workspace' },
|
|
|
+ policy: { mode: 'workspace-write', sessionId: 'agent', workspaceRoot: resolve('/workspace') },
|
|
|
}])
|
|
|
})
|
|
|
|
|
|
@@ -243,11 +245,11 @@ describe('LocalPtyBackend startup rollback', () => {
|
|
|
|
|
|
expect(spawned).toMatchObject({
|
|
|
argv: ['/sandbox', '--', '/bin/bash', '-i'],
|
|
|
- cwd: '/session-workspace',
|
|
|
+ cwd: resolve('/session-workspace'),
|
|
|
})
|
|
|
expect((ctx.sandbox as RecordingSandbox).calls).toEqual([{
|
|
|
argv: ['/bin/bash', '-i'],
|
|
|
- policy: { mode: 'workspace-write', sessionId: 'agent', workspaceRoot: '/session-workspace' },
|
|
|
+ policy: { mode: 'workspace-write', sessionId: 'agent', workspaceRoot: resolve('/session-workspace') },
|
|
|
}])
|
|
|
})
|
|
|
|
|
|
@@ -337,6 +339,99 @@ describe('LocalPtyBackend startup rollback', () => {
|
|
|
expect(session.motd).toBe('dsh> ')
|
|
|
await session.close('test complete')
|
|
|
})
|
|
|
+
|
|
|
+ it('bootstraps a pwsh dialect through the prompt function and scrubs bash-only env', async () => {
|
|
|
+ const ctx = new Context()
|
|
|
+ await ctx.plugin(EmptySandbox)
|
|
|
+ await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' })
|
|
|
+ let spawned: SubprocessTerminalSpawnSpec | undefined
|
|
|
+ let sent: PtySendRequest | undefined
|
|
|
+ const session = {
|
|
|
+ motd: '',
|
|
|
+ startSend: (request: PtySendRequest) => {
|
|
|
+ sent = request
|
|
|
+ return {
|
|
|
+ done: Promise.resolve({
|
|
|
+ viewport: 'setup-echo dsh> ', waitReason: 'stdin_read' as const,
|
|
|
+ sessionStatus: { kind: 'running' as const }, truncated: false,
|
|
|
+ }),
|
|
|
+ readOutput: () => ({ delta: '', truncated: false }),
|
|
|
+ cancel: () => false,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ read: () => ({ text: '', totalLines: 0, lineBegin: 0, lineEnd: 0, truncated: false }),
|
|
|
+ } as unknown as LocalPtySession
|
|
|
+ const backend = new LocalPtyBackend(
|
|
|
+ ctx,
|
|
|
+ { ...config(), shellDialect: 'pwsh', shellPath: 'pwsh' },
|
|
|
+ async (spec) => { spawned = spec; return terminalHandle() },
|
|
|
+ () => session,
|
|
|
+ )
|
|
|
+ expect(await backend.spawn(spec(agent(ctx)))).toBe(session)
|
|
|
+ expect(sent).toMatchObject({ text: PWSH_PROMPT_SETUP, submit: true })
|
|
|
+ expect(session.motd).toBe('setup-echo dsh> ')
|
|
|
+ expect(spawned?.env).toMatchObject({
|
|
|
+ TERM: 'dumb', NO_COLOR: '1', DSH_SHELL: '1', DSH_SESSION_ID: 'agent', DSH_PTY_SESSION_ID: 'pty-1',
|
|
|
+ })
|
|
|
+ expect(spawned?.env?.PS1).toBeUndefined()
|
|
|
+ expect(spawned?.env?.PROMPT_COMMAND).toBeUndefined()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps waiting for the marker prompt when the first send settles on silence', async () => {
|
|
|
+ const ctx = new Context()
|
|
|
+ await ctx.plugin(EmptySandbox)
|
|
|
+ await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' })
|
|
|
+ const sends: PtySendRequest[] = []
|
|
|
+ const session = {
|
|
|
+ motd: '',
|
|
|
+ startSend: (request: PtySendRequest) => {
|
|
|
+ sends.push(request)
|
|
|
+ const second = sends.length > 1
|
|
|
+ return {
|
|
|
+ done: Promise.resolve({
|
|
|
+ viewport: second ? 'dsh> ' : 'PowerShell 7.6.4\n',
|
|
|
+ waitReason: 'inferred_idle' as const,
|
|
|
+ sessionStatus: { kind: 'running' as const }, truncated: false,
|
|
|
+ }),
|
|
|
+ readOutput: () => ({ delta: '', truncated: false }),
|
|
|
+ cancel: () => false,
|
|
|
+ }
|
|
|
+ },
|
|
|
+ read: () => ({ text: '', totalLines: 0, lineBegin: 0, lineEnd: 0, truncated: false }),
|
|
|
+ } as unknown as LocalPtySession
|
|
|
+ const backend = new LocalPtyBackend(
|
|
|
+ ctx,
|
|
|
+ { ...config(), shellDialect: 'pwsh', shellPath: 'pwsh' },
|
|
|
+ async () => terminalHandle(),
|
|
|
+ () => session,
|
|
|
+ )
|
|
|
+ await backend.spawn(spec(agent(ctx)))
|
|
|
+ expect(sends).toHaveLength(2)
|
|
|
+ expect(sends[1]).toMatchObject({ text: '', submit: false })
|
|
|
+ expect(session.motd).toBe('dsh> ')
|
|
|
+ })
|
|
|
+
|
|
|
+ it('rejects a pwsh bootstrap whose shell exits or times out', async () => {
|
|
|
+ const ctx = new Context()
|
|
|
+ await ctx.plugin(EmptySandbox)
|
|
|
+ await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/workspace' })
|
|
|
+ const sessionFor = (waitReason: PtyWaitReason): LocalPtySession => ({
|
|
|
+ startSend: () => ({
|
|
|
+ done: Promise.resolve({
|
|
|
+ viewport: 'no-prompt', waitReason,
|
|
|
+ sessionStatus: { kind: 'running' as const }, truncated: false,
|
|
|
+ }),
|
|
|
+ readOutput: () => ({ delta: '', truncated: false }),
|
|
|
+ cancel: () => false,
|
|
|
+ }),
|
|
|
+ read: () => ({ text: '', totalLines: 0, lineBegin: 0, lineEnd: 0, truncated: false }),
|
|
|
+ close: () => Promise.resolve(),
|
|
|
+ }) as unknown as LocalPtySession
|
|
|
+ const exited = new LocalPtyBackend(ctx, { ...config(), shellDialect: 'pwsh' }, async () => terminalHandle(), () => sessionFor('session_exit'))
|
|
|
+ await expect(exited.spawn(spec(agent(ctx)))).rejects.toThrow('PTY shell exited during startup')
|
|
|
+ const timedOut = new LocalPtyBackend(ctx, { ...config(), shellDialect: 'pwsh' }, async () => terminalHandle(), () => sessionFor('timeout'))
|
|
|
+ await expect(timedOut.spawn(spec(agent(ctx)))).rejects.toThrow('did not reach readiness before startup timeout')
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
describe('pty-local plugin shape', () => {
|