diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 7b98e721d369..ba492f1b7fcb 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -2,6 +2,7 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { describe, expect } from "bun:test" import { Cause, Effect, Exit, Layer } from "effect" import type * as Scope from "effect/Scope" +import fs from "fs" import os from "os" import path from "path" import { Config } from "@/config/config" @@ -1100,29 +1101,48 @@ describe("tool.shell abort", () => { ), ) - it.live("streams metadata updates progressively", () => - runIn( - projectRoot, - Effect.gen(function* () { - const updates: string[] = [] - const result = yield* run( - { - command: `echo first && sleep 0.1 && echo second`, - }, - { - ...ctx, - metadata: (input) => - Effect.sync(() => { - const output = (input.metadata as { output?: string })?.output - if (output) updates.push(output) - }), - }, - ) - expect(result.output).toContain("first") - expect(result.output).toContain("second") - expect(updates.length).toBeGreaterThan(1) - }), - ), + it.live( + "streams metadata updates progressively", + () => + runIn( + projectRoot, + Effect.gen(function* () { + // The second chunk is gated on the test observing the first update, so the + // two writes can never coalesce into a single flush under runner load. + const gate = path + .join(os.tmpdir(), `shell-stream-gate-${process.pid}-${Math.random().toString(36).slice(2)}`) + .replaceAll("\\", "/") + // String.fromCharCode keeps the eval'd code free of quote characters so it + // survives evalarg's quoting under every shell (bash, pwsh, cmd). + const js = (text: string) => `String.fromCharCode(${[...text].map((c) => c.charCodeAt(0)).join(",")})` + const code = `const f=require(${js("fs")});f.writeSync(1,${js("first\n")});while(!f.existsSync(Bun.argv[1]))Bun.sleepSync(50);f.writeSync(1,${js("second\n")})` + const text = `${bin} -e ${evalarg(code)} ${quote(gate)}` + const updates: string[] = [] + let gateWritten = false + const result = yield* run( + { + command: PS.has(sh()) ? `& ${text}` : text, + timeout: 10_000, + }, + { + ...ctx, + metadata: (input) => + Effect.sync(() => { + const output = (input.metadata as { output?: string })?.output + if (output) updates.push(output) + if (output?.includes("first") && !gateWritten) { + gateWritten = true + fs.writeFileSync(gate, "") + } + }), + }, + ).pipe(Effect.ensuring(Effect.sync(() => fs.rmSync(gate, { force: true })))) + expect(result.output).toContain("first") + expect(result.output).toContain("second") + expect(updates.length).toBeGreaterThan(1) + }), + ), + 15_000, ) })