Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/amico-run/src/sota_codebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion packages/amico-run/src/sota_papers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 7 additions & 3 deletions packages/amico-run/test/cloud_verb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
Expand Down
13 changes: 10 additions & 3 deletions packages/amico-run/test/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof parseDoctorArgs>): 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/);
});
});

30 changes: 27 additions & 3 deletions packages/amico-run/test/gh_cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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"),
});
Expand Down
6 changes: 5 additions & 1 deletion packages/amico-run/test/spec_review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 12 additions & 3 deletions packages/extension/test/amicode_service_auth_mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 ────────────
Expand Down
5 changes: 5 additions & 0 deletions packages/extension/test/amicode_service_contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(/\/$/, "");
Expand Down
7 changes: 7 additions & 0 deletions packages/extension/test/amicode_service_engine_proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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");
Expand All @@ -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 () => {
Expand Down
9 changes: 8 additions & 1 deletion packages/extension/test/amicode_service_runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion packages/extension/test/amicode_service_wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) } };
Expand Down
16 changes: 15 additions & 1 deletion packages/extension/test/cli_gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
Expand Down
6 changes: 5 additions & 1 deletion packages/extension/test/open_threads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
18 changes: 18 additions & 0 deletions packages/extension/test/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" });

Expand Down
Loading