|
|
@@ -1,4 +1,4 @@
|
|
|
-import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs'
|
|
|
+import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
import { spawnSync } from 'node:child_process'
|
|
|
import { tmpdir } from 'node:os'
|
|
|
import { join } from 'node:path'
|
|
|
@@ -318,7 +318,7 @@ const hasPwsh = spawnSync(
|
|
|
).status === 0
|
|
|
|
|
|
describe.skipIf(!hasPwsh)('terminal-bash pwsh real shell', () => {
|
|
|
- it('bootstraps a persistent pwsh, persists state, and scrubs secrets', async () => {
|
|
|
+ it.each([false, true])('bootstraps a persistent pwsh, persists state, and scrubs secrets (hold command: %s)', async (holdCommand) => {
|
|
|
const previous = process.env.DSH_TEST_SECRET
|
|
|
process.env.DSH_TEST_SECRET = 'must-not-leak'
|
|
|
try {
|
|
|
@@ -330,21 +330,33 @@ describe.skipIf(!hasPwsh)('terminal-bash pwsh real shell', () => {
|
|
|
const created = await ctx.terminals.spawn(agent, { type: 'shell', name: 'main', cwd: root })
|
|
|
expect(created.motd).toContain('dsh> ')
|
|
|
|
|
|
+ const releaseFile = join(root, 'release-command')
|
|
|
+ // Hold the command across the silence settlement without relying on host load.
|
|
|
+ const barrier = holdCommand
|
|
|
+ ? `while (-not [IO.File]::Exists('${releaseFile.replaceAll("'", "''")}')) { [Threading.Thread]::Sleep(10) }; `
|
|
|
+ : ''
|
|
|
const first = ctx.terminals.startSend(agent, created.sessionId, {
|
|
|
- text: '$env:KEEP = "ok"; Set-Location /',
|
|
|
- submit: true,
|
|
|
- })
|
|
|
- expect((await first.done).waitReason).toBe('stdin_read')
|
|
|
- const second = ctx.terminals.startSend(agent, created.sessionId, {
|
|
|
- text: 'Write-Output "keep=$env:KEEP secret=$env:DSH_TEST_SECRET"',
|
|
|
+ text: barrier + '$env:KEEP = "ok"; Set-Location /',
|
|
|
submit: true,
|
|
|
})
|
|
|
+ expect(['stdin_read', 'inferred_idle']).toContain((await first.done).waitReason)
|
|
|
+ const expected = 'keep=ok cwd=/ secret=END'
|
|
|
+ const command = "Write-Output ('keep={0} cwd={1} secret={2}END' -f $env:KEEP, (Get-Location).Path, $env:DSH_TEST_SECRET)"
|
|
|
+ expect(command).not.toContain(expected)
|
|
|
+ const second = ctx.terminals.startSend(agent, created.sessionId, { text: command, submit: true })
|
|
|
const result = await second.done
|
|
|
- expect(result.viewport).toContain('keep=ok')
|
|
|
- expect(result.viewport).toContain('secret=')
|
|
|
- expect(result.viewport).not.toContain('must-not-leak')
|
|
|
+ expect(['stdin_read', 'inferred_idle']).toContain(result.waitReason)
|
|
|
+ if (holdCommand) {
|
|
|
+ expect(result.waitReason).toBe('inferred_idle')
|
|
|
+ expect(result.viewport).not.toContain(expected)
|
|
|
+ writeFileSync(releaseFile, '')
|
|
|
+ }
|
|
|
|
|
|
- expect(ctx.terminals.read(agent, created.sessionId, { offset: 0, count: 40 }).text).toContain('keep=ok')
|
|
|
+ // A silence-settled send stops collecting output; scrollback still receives
|
|
|
+ // the command's later output. Only the child can produce this formatted token.
|
|
|
+ const read = () => ctx.terminals.read(agent, created.sessionId, { offset: 0, count: 100 }).text
|
|
|
+ await expect.poll(read, { timeout: 8_000 }).toContain(expected)
|
|
|
+ expect(read()).not.toContain('must-not-leak')
|
|
|
expect(await ctx.terminals.kill(agent, created.sessionId)).toBe(true)
|
|
|
expect(ctx.terminals.list(agent)).toEqual([])
|
|
|
} finally {
|