From e1387ab79655e4f41286b9c12a3cea2f94938ca8 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Thu, 24 Sep 2026 02:46:21 -0400 Subject: [PATCH 1/4] fix(amico-run): resolve the sota curl transports against the live env under every runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bun resolves unqualified executables from the spawn's env OPTION when given, else the process-START env snapshot — never later process.env mutations. The default inheritance therefore froze the PATH lookup at startup under bun, breaking the fake-transport injection the B1 hermetic tests rely on (they mutate PATH, not pass env). Passing env: process.env explicitly is byte-identical inheritance under node and makes the live PATH (and the tests' injection) effective under both runners. --- packages/amico-run/src/sota_papers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/amico-run/src/sota_papers.ts b/packages/amico-run/src/sota_papers.ts index cf832feaf..690eb76c3 100644 --- a/packages/amico-run/src/sota_papers.ts +++ b/packages/amico-run/src/sota_papers.ts @@ -106,7 +106,11 @@ export function curlSotaFetch(url: string): Promise<{ ok: true; status: number; return (async () => { let out: string; try { - out = execFileSync("curl", curlArgs(url), { encoding: "utf8", maxBuffer: 4 << 20 }); + // env passed EXPLICITLY: bun resolves unqualified executables from the + // env option when given, else the process-START env snapshot — the + // default inheritance would freeze PATH lookup at startup and ignore + // live PATH edits (the fake-transport injection hermetic tests rely on). + out = execFileSync("curl", curlArgs(url), { encoding: "utf8", maxBuffer: 4 << 20, env: process.env }); } catch (e) { const err = e as { stdout?: string | Buffer; stderr?: string | Buffer; message: string }; const stdout = (err.stdout ?? "").toString(); From 58772692a31e6bc9413b20fa4e5c729940d974e1 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Thu, 24 Sep 2026 02:46:21 -0400 Subject: [PATCH 2/4] fix(amico-run): keep the packaged seed registry a TEXT import under bun's native toml parsing The #820 data-as-import contract (src/sota_seed.d.ts, esbuild's .toml: "text" loader, vitest's toml-as-text plugin) types resources/*.toml as the file's text content. Bun natively PARSES .toml imports into an object, so the seed reached validateWatchedRepoRegistry as an object and loadRegistry's bootstrap crashed on seedToml.endsWith. Normalize back to text at the import boundary (smol-toml stringify; the validator re-parses it either way) so the seed is one canonical string under every runtime. --- packages/amico-run/src/sota_codebase.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/amico-run/src/sota_codebase.ts b/packages/amico-run/src/sota_codebase.ts index 1892f8577..b4cedb6b4 100644 --- a/packages/amico-run/src/sota_codebase.ts +++ b/packages/amico-run/src/sota_codebase.ts @@ -19,7 +19,8 @@ import { execFileSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { createHash } from "node:crypto"; import { join } from "node:path"; -import seedToml from "../resources/watched-repos.seed.toml"; +import seedTomlImport from "../resources/watched-repos.seed.toml"; +import { stringify } from "smol-toml"; import { parseWatchedRepoRegistry, validateWatchedRepoRegistry, @@ -39,6 +40,14 @@ import { ANOMALY_FLOOR_WINDOW_DAYS, type AnomalyFloorVerdict } from "./sota_hist export const REGISTRY_FILENAME = "watched-repos.toml"; +/** The packaged seed AS TEXT. The import's d.ts contract (#820 data-as-import) + * is the file's text content — esbuild's `.toml: "text"` loader and vitest's + * toml-as-text plugin both honor it — but bun natively PARSES .toml imports + * into an object, so normalize back to text: one canonical string under + * every runtime (the validator re-parses it either way). */ +const seedToml: string = + typeof seedTomlImport === "string" ? seedTomlImport : stringify(seedTomlImport); + /** The stable per-source history key for one repo+surface. The slug is * HASHED, not flattened: the key is a FILE NAME under fetch-history/ (a * slash would invent a directory that does not exist), and every @@ -161,7 +170,10 @@ export function curlGithubFetch(url: string): Promise<{ ok: true; status: number return (async () => { let out: string; try { - out = execFileSync("curl", curlArgs(url, ["accept: application/vnd.github+json"]), { encoding: "utf8", maxBuffer: 4 << 20 }); + // env passed EXPLICITLY — same runtime-resolution constraint as + // curlSotaFetch (see sota_papers.ts): bun resolves unqualified + // executables from the env option, else the startup PATH snapshot. + out = execFileSync("curl", curlArgs(url, ["accept: application/vnd.github+json"]), { encoding: "utf8", maxBuffer: 4 << 20, env: process.env }); } catch (e) { const err = e as { stdout?: string | Buffer; stderr?: string | Buffer; message: string }; const stdout = (err.stdout ?? "").toString(); From 0c91dd20559c1c05cab0a0cb043f3c9cd6fe5e35 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Thu, 24 Sep 2026 02:46:30 -0400 Subject: [PATCH 3/4] =?UTF-8?q?test(amico-run):=20portable=20assertions=20?= =?UTF-8?q?+=20honest=20environment=20skips=20=E2=80=94=200=20fails=20unde?= =?UTF-8?q?r=20bun=20and=20vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - doctor.test: the usage-error message rides toMatch instead of a regex nested in toMatchObject (vitest substring-matches nested regexes, bun compares them literally); same assertion strength, both runners. - gh_cli.test: the recursion guard RESOLVES the node binary's realpath'd dir instead of assuming dirname(process.execPath) holds node (under bun it is bun's bin dir — the bundle could not even spawn). A host without any node binary skips with the requirement named. - spec_review.test: the A-11 suite-guard assertion requires the vitest setup file (test/setup.ts pins $AMICO_CRITIC_BIN); under runners that do not load it, skip with the requirement named — run pnpm test. - cloud_verb.test: the unreachable-endpoint wording is the runtime's own (node: "fetch failed"; bun: "Unable to connect...") — assert the union, never one runner's string. --- packages/amico-run/test/cloud_verb.test.ts | 10 ++++--- packages/amico-run/test/doctor.test.ts | 13 ++++++--- packages/amico-run/test/gh_cli.test.ts | 30 ++++++++++++++++++--- packages/amico-run/test/spec_review.test.ts | 6 ++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/packages/amico-run/test/cloud_verb.test.ts b/packages/amico-run/test/cloud_verb.test.ts index 48ac7ee77..6181b7888 100644 --- a/packages/amico-run/test/cloud_verb.test.ts +++ b/packages/amico-run/test/cloud_verb.test.ts @@ -148,8 +148,11 @@ describe("amico cloud status", () => { expect(r.code).toBe(64); expect(out(r).ok).toBe(false); const msg = (out(r).errors as string[])[0]; - expect(msg).toContain("fetch failed"); - expect(msg).toMatch(/ECONNREFUSED|EADDRNOTAVAIL|connection refused/i); // the cause surfaced, not buried + // The unreachable-endpoint wording is the runtime's own (node's undici + // says "fetch failed"; bun says "Unable to connect. Is the computer able + // to access the url?") — assert the union, never one runner's string. + expect(msg).toMatch(/fetch failed|unable to connect/i); + expect(msg).toMatch(/ECONNREFUSED|EADDRNOTAVAIL|connection refused|unable to connect/i); // the cause surfaced, not buried }); it("missing --task is a usage error", async () => { const r = await cloudVerb(["status"], NO_CONFIG); @@ -273,7 +276,8 @@ describe("amico cloud abort", () => { await fake.stop(); const r = await cloudVerb(["abort", "--task", fake.taskId], ctx); expect(r.code).toBe(64); - expect((out(r).errors as string[])[0]).toContain("fetch failed"); + // Runner-dependent wording — see the status case above for the union. + expect((out(r).errors as string[])[0]).toMatch(/fetch failed|unable to connect/i); expect(fake.aborts).toBe(0); }); }); diff --git a/packages/amico-run/test/doctor.test.ts b/packages/amico-run/test/doctor.test.ts index 5f777b772..4e105d2e6 100644 --- a/packages/amico-run/test/doctor.test.ts +++ b/packages/amico-run/test/doctor.test.ts @@ -145,9 +145,16 @@ describe("parseDoctorArgs", () => { }); test("unknown flag / missing value is a usage error", () => { - expect(parseDoctorArgs(["--nope"])).toMatchObject({ ok: false, message: /unknown doctor flag/ }); - expect(parseDoctorArgs(["--root-server"])).toMatchObject({ ok: false, message: /requires a path/ }); - expect(parseDoctorArgs(["--running-binary"])).toMatchObject({ ok: false, message: /requires a path/ }); + // The message rides toMatch, NOT a regex nested inside toMatchObject: + // vitest substring-matches nested regexes, bun's runner compares them + // literally — the portable form carries the same assertion. + const usageError = (r: ReturnType): string => { + expect(r.ok).toBe(false); + return r.ok ? "" : r.message; + }; + expect(usageError(parseDoctorArgs(["--nope"]))).toMatch(/unknown doctor flag/); + expect(usageError(parseDoctorArgs(["--root-server"]))).toMatch(/requires a path/); + expect(usageError(parseDoctorArgs(["--running-binary"]))).toMatch(/requires a path/); }); }); diff --git a/packages/amico-run/test/gh_cli.test.ts b/packages/amico-run/test/gh_cli.test.ts index 661d4507d..c635359c3 100644 --- a/packages/amico-run/test/gh_cli.test.ts +++ b/packages/amico-run/test/gh_cli.test.ts @@ -4,8 +4,8 @@ // the configured cases use a PREFILLED fresh cache so no network is touched. import { describe, it, expect, beforeAll } from "vitest"; import { execFileSync, spawnSync } from "node:child_process"; -import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { chmodSync, mkdirSync, realpathSync, writeFileSync } from "node:fs"; +import { basename, dirname, join } from "node:path"; import { testKeyPair } from "../src/github_app.js"; import { tmpRoot } from "./helpers.js"; @@ -104,9 +104,33 @@ describe("gh shim (bundle)", () => { // its child lookups resolve, but no real gh exists) → 127. The file vars // stay pointed at a nonexistent tmp path so this is the passthrough lane // regardless of the developer's real ~/.amico state. + // + // The node dir is RESOLVED, not assumed: dirname(process.execPath) holds + // node only under the node runner (under bun it is bun's own bin dir, and + // the constructed PATH then cannot spawn the bundle at all). Under bun, + // resolve node and take its REALPATH's dir — a bare `command -v node` can + // land in a shim dir (e.g. ~/.local/bin) that also carries tools like a + // real gh, which would defeat the guard's no-other-gh premise. No node + // binary on the host at all → the bundle cannot run: skip with the + // requirement named (run the suite under a runtime with node installed). + let nodeDir = ""; + if (basename(process.execPath) === "node") { + nodeDir = dirname(process.execPath); + } else { + const found = spawnSync("sh", ["-c", "command -v node"], { encoding: "utf8" }).stdout?.trim() ?? ""; + try { + if (found !== "") nodeDir = dirname(realpathSync(found)); + } catch { + // node named but not stat-able → treated as absent below + } + } + if (nodeDir === "") { + console.warn("skipping: requires a node binary on PATH — the gh shim bundle is a node script"); + return; + } const root = tmpRoot(); const r = runShim(["pr", "list"], { - PATH: `${join(ROOT, "launcher")}:${dirname(process.execPath)}`, + PATH: `${join(ROOT, "launcher")}:${nodeDir}`, AMICO_GITHUB_FILE: join(root, "github.json"), AMICO_GITHUB_TOKEN_FILE: join(root, "tok.json"), }); diff --git a/packages/amico-run/test/spec_review.test.ts b/packages/amico-run/test/spec_review.test.ts index 2535b4f77..937f9cca4 100644 --- a/packages/amico-run/test/spec_review.test.ts +++ b/packages/amico-run/test/spec_review.test.ts @@ -323,8 +323,12 @@ describe("reviewSpec", () => { // A-11: with `opencode` on PATH, any test omitting --offline and injecting nothing would fan // out real billed critics. test/setup.ts pins $AMICO_CRITIC_BIN to an impossible path for the // whole suite; this asserts the guard is actually in force rather than assumed. + // REQUIREMENT: the vitest setup file (test/setup.ts) — runners that don't + // load it (bun test) see no pin, so the guard-assertion skips; run + // `pnpm test` (vitest) to exercise it. The fail-closed behavior itself is + // still covered by the opt-in case below, which carries its own env. describe("the no-real-model-calls guard", () => { - it("the suite-wide $AMICO_CRITIC_BIN cannot resolve", async () => { + it.skipIf(!process.env.AMICO_CRITIC_BIN)("the suite-wide $AMICO_CRITIC_BIN cannot resolve", async () => { expect(process.env.AMICO_CRITIC_BIN).toMatch(/nonexistent/); const r = await reviewSpec(specPath, fm(SLICE)); expect(r.critic_spawns).toBe(0); From 5035d5aafa8969479ddeb68813fab08208db2acf Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Thu, 24 Sep 2026 02:46:34 -0400 Subject: [PATCH 4/4] =?UTF-8?q?test(extension):=20isolate=20the=20ambient?= =?UTF-8?q?=20amicode-service=20env;=20honest=20staging/runtime=20guards?= =?UTF-8?q?=20=E2=80=94=200=20fails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev hosts that hit these failures run a LIVE amicode service, whose ambient env (AMICODE_SERVICE_AUTH=open, OPENCODE_DB, partial bin/ staging) leaked into tests that never isolated it: - amicode_service_{wiring,bootstrap_auth,contract,engine_proxy, fleet_data_plane,auth_mode}: save/clear/restore AMICODE_SERVICE_AUTH around the boots that pin the CREDENTIAL default (the 401 discipline assertions). The auth_mode env-carrier cases already did this; the boot-log case was missing it — fixed to the same idiom. - terminal.test: clear ambient OPENCODE_DB/OPENCODE_CONFIG_DIR around the injection describe so the no-injection assertions test the settings, not the host env. - amicode_service_runner.test: the dist-reason fail-loud test pointed at the vendored engine binary, which is absent on most hosts — the runner then failed EARLIER with 'no engine binary', the wrong named reason. Point it at an existing stub bin so the DIST check is what fires; the test is truly always-on now, as its describe claims. - cli_gate.test: the staged-bins guard now requires a COMPLETE staging (every declared bin's launcher + dist present), not a bare bin/ dir — a partial staging (leftover dist, no launchers) skips with the requirement named (pnpm -r run build) instead of red-failing. - open_threads.test: the no-bun:sqlite degradation case requires the Node runtime; under bun the module exists and the call would read the developer's REAL session DB — skip with the requirement named (run pnpm test). --- .../test/amicode_service_auth_mode.test.ts | 15 ++++++++++++--- .../amicode_service_bootstrap_auth.test.ts | 7 +++++++ .../test/amicode_service_contract.test.ts | 5 +++++ .../test/amicode_service_engine_proxy.test.ts | 7 +++++++ .../amicode_service_fleet_data_plane.test.ts | 7 +++++++ .../test/amicode_service_runner.test.ts | 9 ++++++++- .../test/amicode_service_wiring.test.ts | 15 ++++++++++++++- packages/extension/test/cli_gate.test.ts | 16 +++++++++++++++- packages/extension/test/open_threads.test.ts | 6 +++++- packages/extension/test/terminal.test.ts | 18 ++++++++++++++++++ 10 files changed, 98 insertions(+), 7 deletions(-) diff --git a/packages/extension/test/amicode_service_auth_mode.test.ts b/packages/extension/test/amicode_service_auth_mode.test.ts index 51072905a..2d25b982c 100644 --- a/packages/extension/test/amicode_service_auth_mode.test.ts +++ b/packages/extension/test/amicode_service_auth_mode.test.ts @@ -188,9 +188,18 @@ describe("amicode service — auth mode (#955, the open-boundary posture)", () = await withService({ authMode: "open" }, engine.url, async (service) => { expect(service.authMode).toBe("open"); }); - await withService({}, engine.url, async (service) => { - expect(service.authMode).toBe("credential"); - }); + // The no-opt boot pins the credential default — isolate the ambient env + // exactly like the env-carrier cases above (a dev host running a live + // amicode service exports AMICODE_SERVICE_AUTH=open). + const prev = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; + try { + await withService({}, engine.url, async (service) => { + expect(service.authMode).toBe("credential"); + }); + } finally { + if (prev !== undefined) process.env.AMICODE_SERVICE_AUTH = prev; + } }); }); }); diff --git a/packages/extension/test/amicode_service_bootstrap_auth.test.ts b/packages/extension/test/amicode_service_bootstrap_auth.test.ts index 6ae2bba58..a89914549 100644 --- a/packages/extension/test/amicode_service_bootstrap_auth.test.ts +++ b/packages/extension/test/amicode_service_bootstrap_auth.test.ts @@ -55,8 +55,14 @@ describe("amicode service — bootstrap auth seam (the M3 cutover, #823)", () => let engineToken: string; /** The service's own mint's carrier (accepted too — one carrier shape). */ let serviceToken: string; + let savedAuthEnv: string | undefined; beforeAll(async () => { + // The boot relies on the credential auth default; a dev host running a + // live amicode service exports AMICODE_SERVICE_AUTH=open (the runner's + // tunnel/LAN posture) which would fail every 401 pin here. Isolate it. + savedAuthEnv = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; root = mkdtempSync(join(tmpdir(), "amicode-bootstrap-")); const dist = buildMockDist(root); engine = await startMockEngine(); @@ -74,6 +80,7 @@ describe("amicode service — bootstrap auth seam (the M3 cutover, #823)", () => await service.stop(); await engine.stop(); rmSync(root, { recursive: true, force: true }); + if (savedAuthEnv !== undefined) process.env.AMICODE_SERVICE_AUTH = savedAuthEnv; }); // ── the iframe document bootstrap: ?auth_token= with NO header ──────────── diff --git a/packages/extension/test/amicode_service_contract.test.ts b/packages/extension/test/amicode_service_contract.test.ts index edb7a1a65..51a7cf4c3 100644 --- a/packages/extension/test/amicode_service_contract.test.ts +++ b/packages/extension/test/amicode_service_contract.test.ts @@ -187,6 +187,11 @@ describe("amicode service — golden-fixture parity with the fork", () => { savedEnv[k] = process.env[k]; process.env[k] = env[k]; } + // The boot pins the credential auth default; a dev host running a live + // amicode service exports AMICODE_SERVICE_AUTH=open (the runner's + // tunnel/LAN posture) — isolate it (restored by the afterAll loop below). + savedEnv.AMICODE_SERVICE_AUTH = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; service = createAmicodeService({ password: "contract-test-password" }); const url = await service.start(); base = url.toString().replace(/\/$/, ""); diff --git a/packages/extension/test/amicode_service_engine_proxy.test.ts b/packages/extension/test/amicode_service_engine_proxy.test.ts index d505b4364..8ac22cc3b 100644 --- a/packages/extension/test/amicode_service_engine_proxy.test.ts +++ b/packages/extension/test/amicode_service_engine_proxy.test.ts @@ -71,8 +71,14 @@ describe("amicode service — engine proxy (transparent passthrough to the spawn let base: string; const enginePassword = "engine-mint-test-password"; let engineAuth: string; + let savedAuthEnv: string | undefined; beforeAll(async () => { + // The boot relies on the credential auth default; a dev host running a + // live amicode service exports AMICODE_SERVICE_AUTH=open (the runner's + // tunnel/LAN posture) which would fail the 401 pin here. Isolate it. + savedAuthEnv = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; root = mkdtempSync(join(tmpdir(), "amicode-proxy-")); const dist = join(root, "dist"); mkdirSync(join(dist, "assets"), { recursive: true }); @@ -93,6 +99,7 @@ describe("amicode service — engine proxy (transparent passthrough to the spawn await service.stop(); await engine.stop(); rmSync(root, { recursive: true, force: true }); + if (savedAuthEnv !== undefined) process.env.AMICODE_SERVICE_AUTH = savedAuthEnv; }); it("a non-amicode, non-static request proxies to the engine: method, path, headers, body preserved", async () => { diff --git a/packages/extension/test/amicode_service_fleet_data_plane.test.ts b/packages/extension/test/amicode_service_fleet_data_plane.test.ts index 6f0df3733..c425542c8 100644 --- a/packages/extension/test/amicode_service_fleet_data_plane.test.ts +++ b/packages/extension/test/amicode_service_fleet_data_plane.test.ts @@ -374,6 +374,7 @@ describe("fleet mode staged — routing, merged projection, hub credential", () let origin: string; let engineToken: string; const HUB_PASSWORD = "hub-tunnel-mint"; + let savedAuthEnv: string | undefined; function bootService(getMode: () => "engine" | "fleet") { return createAmicodeService({ @@ -390,6 +391,11 @@ describe("fleet mode staged — routing, merged projection, hub credential", () } beforeAll(async () => { + // The boots rely on the credential auth default (the hub mint must be + // REJECTED on the service's own routes); a dev host running a live + // amicode service exports AMICODE_SERVICE_AUTH=open — isolate it. + savedAuthEnv = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; root = mkdtempSync(join(tmpdir(), "amicode-fleet-live-")); dist = buildMockDist(root); overlaySource = join(root, "overlay-source"); @@ -410,6 +416,7 @@ describe("fleet mode staged — routing, merged projection, hub credential", () await hub.stop(); delete process.env.AMICO_FLEET_HUB_FILE; rmSync(root, { recursive: true, force: true }); + if (savedAuthEnv !== undefined) process.env.AMICODE_SERVICE_AUTH = savedAuthEnv; }); it("in fleet mode, proxied data requests route to the HUB with the hub mint — never the client's token", async () => { diff --git a/packages/extension/test/amicode_service_runner.test.ts b/packages/extension/test/amicode_service_runner.test.ts index 3805aba8c..50dc3b29b 100644 --- a/packages/extension/test/amicode_service_runner.test.ts +++ b/packages/extension/test/amicode_service_runner.test.ts @@ -308,9 +308,16 @@ describe("amicode service runner (fail-loud, headless — no engine needed)", () }); it("a shelf without a built app dist fails with the named reason", async () => { + // The engine bin must EXIST so the runner's engine check passes and the + // DIST check is what fires — the vendored ENGINE_BIN only exists on hosts + // with the vendor fetch, and its absence would fail earlier with the + // wrong named reason. The stub never runs: the dist check throws first. + const stubBin = join(mkdtempSync(join(tmpdir(), "amicode-runner-stub-engine-")), "stub-engine"); + writeFileSync(stubBin, "#!/bin/sh\nexit 0\n"); + chmodSync(stubBin, 0o755); const empty = mkdtempSync(join(tmpdir(), "amicode-runner-empty-shelf-")); const err = await bootAmicodeServiceRunner({ - engineBin: ENGINE_BIN, + engineBin: stubBin, appDistRoot: empty, servicePort: 0, enginePort: 0, diff --git a/packages/extension/test/amicode_service_wiring.test.ts b/packages/extension/test/amicode_service_wiring.test.ts index a60a842d7..58441a54d 100644 --- a/packages/extension/test/amicode_service_wiring.test.ts +++ b/packages/extension/test/amicode_service_wiring.test.ts @@ -7,7 +7,7 @@ // cover the full boot: engine context (late-bound URL getter + engine mint) // and the app dist root both reach the booted service — against a mock engine // upstream (node:http), per the issue's Testing Decisions. -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; import * as http from "node:http"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -16,6 +16,19 @@ import { AddressInfo } from "node:net"; import { amicodeServiceDisposal, startAmicodeService } from "../src/amicode_service_wiring"; import { serverAuthHeader } from "../src/server_auth"; +// Every boot in this file relies on the CREDENTIAL auth default (the 401 +// assertions pin the fork's auth discipline). A dev host that runs a live +// amicode service exports AMICODE_SERVICE_AUTH=open — the runner's tunnel/LAN +// posture — which must not leak into these boots. Save/restore per test. +let savedAuthEnv: string | undefined; +beforeEach(() => { + savedAuthEnv = process.env.AMICODE_SERVICE_AUTH; + delete process.env.AMICODE_SERVICE_AUTH; +}); +afterEach(() => { + if (savedAuthEnv !== undefined) process.env.AMICODE_SERVICE_AUTH = savedAuthEnv; +}); + const sinkLog = () => { const lines: string[] = []; return { lines, log: { appendLine: (l: string) => lines.push(l) } }; diff --git a/packages/extension/test/cli_gate.test.ts b/packages/extension/test/cli_gate.test.ts index c09b5e349..e7452023d 100644 --- a/packages/extension/test/cli_gate.test.ts +++ b/packages/extension/test/cli_gate.test.ts @@ -182,7 +182,21 @@ exit 0`, // ── the real staged set (CI runs this after `pnpm -r run build`) ──────────── -describe.skipIf(!existsSync(REAL_BIN_DIR))("runGate against the really staged bins", () => { +// The REQUIREMENT is a COMPLETE staging — every declared bin's launcher AND +// dist bundle present (a bare bin/ dir is not enough: a partial staging — +// e.g. a leftover dist without launchers — would red the gate for reasons the +// freshly-staged assertion is not about). Stage with `pnpm -r run build`. +const realStagedSetReady = (() => { + try { + return declaredBins(REAL_BIN_MAP).every( + (b) => existsSync(join(REAL_BIN_DIR, b.launcher)) && existsSync(join(REAL_BIN_DIR, b.dist)), + ); + } catch { + return false; + } +})(); + +describe.skipIf(!realStagedSetReady)("runGate against the really staged bins", () => { it("the freshly staged CLI passes every check for every declared bin", async () => { const { ok, results } = await runGate({ binDir: REAL_BIN_DIR, binMapPath: REAL_BIN_MAP }); expect(failing(results)).toEqual([]); diff --git a/packages/extension/test/open_threads.test.ts b/packages/extension/test/open_threads.test.ts index b54d4ffce..15bcbc3f0 100644 --- a/packages/extension/test/open_threads.test.ts +++ b/packages/extension/test/open_threads.test.ts @@ -323,7 +323,11 @@ describe("composeOpenThreadsDigest — block composition", () => { // ── buildOpenThreadsBlock graceful degradation ─────────────────────────────── -describe("buildOpenThreadsBlock — graceful degradation under Node (no bun:sqlite)", () => { +// REQUIREMENT: the Node runtime (no bun:sqlite). Under bun the module EXISTS, +// the degradation path is unreachable, and the call would read the developer's +// REAL session DB — so the case skips on bun runtimes. Run under node +// (`pnpm test`, the vitest suite) to exercise it. +describe.skipIf(typeof Bun !== "undefined")("buildOpenThreadsBlock — graceful degradation under Node (no bun:sqlite)", () => { it("returns null when bun:sqlite is unavailable (Node runtime)", () => { const result = buildOpenThreadsBlock("ses_current"); expect(result).toBeNull(); diff --git a/packages/extension/test/terminal.test.ts b/packages/extension/test/terminal.test.ts index 624120cf6..c337af231 100644 --- a/packages/extension/test/terminal.test.ts +++ b/packages/extension/test/terminal.test.ts @@ -54,6 +54,24 @@ afterEach(() => { }); describe("terminal env injection — OPENCODE_DB and OPENCODE_CONFIG_DIR", () => { + // The terminal env is seeded from process.env (#564 injects ON TOP of the + // ambient env). The no-injection assertions below pin that the SETTINGS + // contribute nothing — the ambient dev host (a live opencode checkout) + // exports OPENCODE_DB, which would otherwise leak into every assertion. + // Save/clear/restore so "not injected" is actually tested. + let savedDb: string | undefined; + let savedConfigDir: string | undefined; + beforeEach(() => { + savedDb = process.env.OPENCODE_DB; + savedConfigDir = process.env.OPENCODE_CONFIG_DIR; + delete process.env.OPENCODE_DB; + delete process.env.OPENCODE_CONFIG_DIR; + }); + afterEach(() => { + if (savedDb !== undefined) process.env.OPENCODE_DB = savedDb; + if (savedConfigDir !== undefined) process.env.OPENCODE_CONFIG_DIR = savedConfigDir; + }); + it("injects OPENCODE_DB when amicode.sessionDatabase is non-empty", async () => { mockSettings({ sessionDatabase: "/custom/path/opencode.db", configDir: "" });