From a69e29024928f1691c51f62490f93cbea2025f03 Mon Sep 17 00:00:00 2001 From: Lacy Morrow Date: Mon, 13 Jul 2026 10:32:53 -0400 Subject: [PATCH 1/2] fix(test): deflake tool.shell metadata streaming test (LAC-2722) The 'streams metadata updates progressively' test raced the scheduler: under runner load both echo chunks arrived within one flush window and coalesced into a single metadata update, failing expect(updates.length).toBeGreaterThan(1). Gate the second chunk on the test observing the first update via a tmpdir sentinel file, so two distinct updates are guaranteed by construction rather than by timing. Co-Authored-By: Paperclip --- packages/opencode/test/tool/shell.test.ts | 57 ++++++++++++++--------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 7b98e721d369..ea37766fc6a6 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,39 @@ 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("\\", "/") + const updates: string[] = [] + const result = yield* run( + { + command: `echo first; until [ -f "${gate}" ]; do sleep 0.05; done; echo second`, + 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")) 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, ) }) From 47867e072bf2fdf7f7004f5144fc4af69b8b48fd Mon Sep 17 00:00:00 2001 From: Lacy Morrow Date: Mon, 13 Jul 2026 10:51:40 -0400 Subject: [PATCH 2/2] fix(test): make streaming fixture cross-shell and gate write idempotent (LAC-2722) Address Gemini review on PR #40: - Replace POSIX-only 'until [ -f ... ]' loop with a bun -e one-liner (charcode-encoded like the existing fill helper) so the fixture parses under bash, pwsh, and cmd on Windows runners - Write the gate file once via a gateWritten flag instead of on every cumulative metadata update --- packages/opencode/test/tool/shell.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index ea37766fc6a6..ba492f1b7fcb 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1112,10 +1112,16 @@ describe("tool.shell abort", () => { 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: `echo first; until [ -f "${gate}" ]; do sleep 0.05; done; echo second`, + command: PS.has(sh()) ? `& ${text}` : text, timeout: 10_000, }, { @@ -1124,7 +1130,10 @@ describe("tool.shell abort", () => { Effect.sync(() => { const output = (input.metadata as { output?: string })?.output if (output) updates.push(output) - if (output?.includes("first")) fs.writeFileSync(gate, "") + if (output?.includes("first") && !gateWritten) { + gateWritten = true + fs.writeFileSync(gate, "") + } }), }, ).pipe(Effect.ensuring(Effect.sync(() => fs.rmSync(gate, { force: true }))))