Răsfoiți Sursa

fix(persistent-bash): wait for complete status markers

Tianyi Cui 1 lună în urmă
părinte
comite
7f5aa2d053

+ 6 - 4
packages/pty/tool-bash-persistent/src/index.ts

@@ -93,16 +93,18 @@ function stripPrompt(text: string): string {
 function commandOutput(
   snapshot: RetainedOutput,
   marker: CommandMarkers,
-): CapturedOutput {
+): CapturedOutput | undefined {
   const text = snapshot.text
   const end = text.lastIndexOf(marker.end)
-  const exitCode = Number.parseInt(text.slice(end + marker.end.length), 10)
+  if (end < 0) return undefined
+  const status = /^(\d+)\r?\n/.exec(text.slice(end + marker.end.length))?.[1]
+  if (status === undefined) return undefined
   const startMarker = text.lastIndexOf(marker.start, end)
   const start = startMarker < 0 ? 0 : startMarker + marker.start.length
   return {
     text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
     incomplete: startMarker < 0,
-    exitCode,
+    exitCode: Number(status),
   }
 }
 
@@ -317,7 +319,7 @@ async function executeCommand(
     }
     if (latest.text.includes(marker.end)) {
       const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker)
-      return renderCaptured(complete, config.maxOutputChars)
+      if (complete !== undefined) return renderCaptured(complete, config.maxOutputChars)
     }
     if (result.sessionStatus.kind === 'exited') {
       const snapshot = retainedScrollback(ctx, owner, id, latest)

+ 22 - 0
packages/pty/tool-bash-persistent/tests/tools.spec.ts

@@ -84,6 +84,8 @@ type StubMode =
   | 'idle-then-normal'
   | 'large'
   | 'nonzero'
+  | 'torn-status'
+  | 'finish-torn-status'
   | 'end-only'
   | 'init-exit'
   | 'init-timeout'
@@ -160,6 +162,17 @@ class StubPtySession implements PtyBackendSession {
     this.pendingText = ''
     const start = /__DSH_PERSISTENT_BASH_START_[^_]+(?:-[^_]+)*__/.exec(sent)?.[0]
     const end = /__DSH_PERSISTENT_BASH_END_[^:]+:/.exec(sent)?.[0]
+    if (this.mode === 'torn-status') {
+      const output = `${start ?? ''}\nhello from stub\n${end ?? ''}`
+      this.scrollback += output
+      this.mode = 'finish-torn-status'
+      return this.operation(Promise.resolve(this.result(output, 'inferred_idle')))
+    }
+    if (this.mode === 'finish-torn-status') {
+      const output = `7\n${this.motd}`
+      this.scrollback += output
+      return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
+    }
     if (this.mode === 'end-only') {
       const output = `recovered output\n${end ?? ''}0\n${this.motd}`
       this.scrollback += output
@@ -345,6 +358,15 @@ describe('tool-bash-persistent', () => {
     expect(stub.sessions[2]?.closed).toEqual(['external cleanup'])
   })
 
+  it('waits for status digits after a torn completion marker', async () => {
+    const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 })
+    await call(ctx, owner, 'warm up')
+    stub.sessions[0]!.mode = 'torn-status'
+    stub.sessions[0]!.scrollback = ''
+
+    expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[exit code: 7]')
+  })
+
   it('marks a short missing-prefix result and tolerates exhausted scrollback pages', async () => {
     const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 })
     await call(ctx, owner, 'warm up')