Skip to content
31 changes: 29 additions & 2 deletions scripts/ci-pretest-cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@
//
// The workflow side (actions/cache keyed on hashFiles over the SAME inputs,
// per runner.os) only accelerates the MISS path across legs; the marker
// check stands on its own, so local `npm test` warm runs benefit too.
// check stands on its own, so local `npm test` warm runs benefit too. One
// side effect it must undo: a restore hands the dist back with SAVE-TIME
// mtimes, older than the just-checked-out sources. tests/golden-storage-drive.ts'
// #3325 staleness guard is a pure MTIME oracle (newest kernel/src vs
// node_modules/acp-kernel/dist/index.js), so the HIT path restamps the
// verified dist tree's mtimes to now — content untouched, the two oracles
// agree again (#4599 sentinel: without this, every cache-hit leg reds the
// storage-render golden chat wire ~4ms into the drive).
//
// Parity note: pretest previously ran `npm --prefix kernel run build && npm
// --prefix pi-subagents run build`; this script runs exactly those commands
Expand All @@ -32,7 +39,7 @@

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, utimesSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

Expand Down Expand Up @@ -128,6 +135,25 @@ export function markerIsFresh(distDir, hash) {
}
}

/**
* Mtime reconciliation after a VERIFIED hit (#4360×#3325): the marker proved
* the bytes are current, but a cache restore left save-time mtimes behind
* the checked-out sources. Restamp every file's mtime to now so any
* mtime-based freshness oracle over the dist agrees with the content oracle.
* Content is never touched.
*/
function restampMtimes(distDir) {
const now = new Date();
const walk = (dir) => {
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.isFile()) utimesSync(p, now, now);
}
};
walk(distDir);
}

/**
* The pretest loop. For each component: hash → fresh marker? HIT, skip :
* MISS → run the real build → stamp the marker. `build`/`log` are test seams
Expand All @@ -144,6 +170,7 @@ export function runPretestCache(opts = {}) {
const hash = hashComponent(root, comp);
const distDir = join(root, comp.dir, "dist");
if (markerIsFresh(distDir, hash)) {
restampMtimes(distDir);
log(`pretest-cache: HIT (${comp.name}) — dist fresh for hash ${hash.slice(0, 12)}`);
results.push({ name: comp.name, hit: true, ms: 0 });
continue;
Expand Down
43 changes: 42 additions & 1 deletion tests/ci-pretest-cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { mkdtempSync, mkdirSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as cache from "../scripts/ci-pretest-cache.mjs";
Expand Down Expand Up @@ -89,6 +89,47 @@ test("cold → warm → tamper lifecycle (the #4360 CI sequence)", () => {
assert.ok(lines.some((l) => l.startsWith("pretest-cache: MISS (kernel) → rebuilding")));
});

// #4599 sentinel (diagnosed on run 40161560956): actions/cache restores a
// verified-fresh dist with SAVE-TIME mtimes while the fresh checkout leaves
// the sources newer — content byte-identical, clock says stale. The #3325
// guard in tests/golden-storage-drive.ts is a pure mtime oracle and reds
// every cache-hit leg's golden drive ~4ms in. A verified HIT must reconcile
// the two oracles by restamping dist mtimes, never by rebuilding or by
// touching bytes.
test("verified HIT restamps dist mtimes ahead of the checked-out sources (#4360×#3325)", () => {
const { root } = makeMiniRepo();
const log = (): void => {};
// COLD: real build materializes dist + marker.
cache.runPretestCache({ root, build: fakeBuild([]), log });

// Simulate the CI sequence: restore hands the dist back 24h old, the
// checkout leaves every source at now.
const old = new Date(Date.now() - 24 * 60 * 60 * 1000);
utimesSync(join(root, "kernel/dist/index.js"), old, old);
for (const rel of ["kernel/src/index.ts", "kernel/src/deep/util.ts", "kernel/tsup.config.ts"]) {
utimesSync(join(root, rel), new Date(), new Date());
}
const srcAt = Math.max(
statSync(join(root, "kernel/src/index.ts")).mtimeMs,
statSync(join(root, "kernel/src/deep/util.ts")).mtimeMs,
statSync(join(root, "kernel/tsup.config.ts")).mtimeMs,
);
const distBefore = statSync(join(root, "kernel/dist/index.js")).mtimeMs;
assert.ok(srcAt > distBefore, "precondition: restored dist sits behind the checked-out sources");

// WARM: marker still byte-exact → HIT, zero builds…
const seen: string[] = [];
const r = cache.runPretestCache({ root, build: fakeBuild(seen), log });
assert.deepEqual(r.map((x) => x.hit), [true, true]);
assert.equal(seen.length, 0);

// …and afterwards the #3325 mtime condition can no longer fire, with
// the dist bytes untouched.
const distAfter = statSync(join(root, "kernel/dist/index.js")).mtimeMs;
assert.ok(distAfter >= srcAt, "HIT must restamp the dist ahead of the checked-out sources");
assert.equal(readFileSync(join(root, "kernel/dist/index.js"), "utf-8"), "built-bytes");
});

test("marker must match byte-exactly — absent, stale, tampered, CRLF are decided by content", () => {
const { root } = makeMiniRepo();
const kernel = cache.COMPONENTS[0];
Expand Down
9 changes: 8 additions & 1 deletion tests/epoch-confirm-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ for (const shape of ["one-interior-survivor", "all-missing", "small-zombie"] as
if (shape === "all-missing") {
assert.equal(session.state.blocks.length, 1);
assert.equal(session.state.blocks[0].active, false);
assert.equal(session.metadata.foldEpochConfirmPending === true, false);
// #3641 doctrine: zero survivors latches the pause too — the
// substrate is maximally unprovable and the ask is which
// history owns the session going forward; folding must not
// silently start a new epoch on foreign bytes. The original
// `false` here contradicted the #3641 doctrine suites
// (fold-anchor-doctrine) and was born-red at #4260; aligned
// to the doctrine instead of carving the implementation out.
assert.equal(session.metadata.foldEpochConfirmPending === true, pass >= 3);
}
}
assert.equal(session.blockContents.get(blockId)?.full.text, original);
Expand Down
24 changes: 13 additions & 11 deletions tests/issue2915-order-centric-reconcile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,21 +360,23 @@ describe("escalation latch recovery (review gap ②) — upgrade-in-place heals"
assert.equal(logs.filter((l) => l.msg.includes("fold epoch confirmation required")).length, 1,
"#2297: exactly ONE error line per episode");

// healthy resend of the ORIGINAL bytes: missing=0 → noop, and the
// streak reset clears the latch — the upgrade-in-place recovery path.
// healthy resend of the ORIGINAL bytes: everything present → fully
// clean pass ("resend"), and the streak reset clears the latch — the
// upgrade-in-place recovery path.
const healthy = reconcileFoldCoverage(session, originals, opts(log));
assert.equal(healthy.kind, "noop");
assert.equal(healthy.kind, "resend");
assert.ok(session.metadata.foldDriftEscalated === undefined, "latch cleared on recovery");
assert.ok(session.metadata.foldDriftStreak === undefined, "streak cleared on recovery");

// Escalation reaped the destroyed block (#2695 zombie-carrier reap) —
// so after the latch clears the ledger is a CLEAN SLATE, not a repair
// target: nothing is covered, nothing missing, and the next fold
// cycle starts fresh. That is the upgrade-in-place contract: a
// v0.1.192→v0.1.196 upgrade with a latched ledger heals instead of
// dead-looping the doomed preflight.
assert.equal((session.state.blocks as unknown[]).length, 0, "destroyed substrate reaped at escalation");
// #4260 superseded the #2695 clean-slate contract: escalation no
// longer reaps — the fold snapshot is RETAINED while the ledger heals
// (nothing is deleted, nothing is silently reset), so after the latch
// clears the session keeps its block and the next id churn re-anchors
// against it instead of starting from scratch. That is the
// upgrade-in-place contract: a v0.1.192→v0.1.196 upgrade with a
// latched ledger heals instead of dead-looping the doomed preflight.
assert.equal((session.state.blocks as unknown[]).length, 1, "#4260: destroyed substrate retained while epoch confirmation pending");
const r = reconcileFoldCoverage(session, remint(originals, "g9"), opts(log));
assert.equal(r.kind, "noop", "clean slate — the lane lives, folds rebuild on the next compress cycle");
assert.equal(r.kind, "reanchored", "retained snapshot re-anchors the id re-mint positionally");
});
});
6 changes: 3 additions & 3 deletions tests/issue3013-refusal-terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ test("A1: refusal bodies carry the bili_final terminal marker on both protocol s
assert.equal(err.code, "dsh_compaction_refused");

// the message must state the terminal semantics + the levers (#3013)
assert.match(String(err.message), /terminal for the session/);
assert.match(String(err.message), /unconditional and terminal \(bili_final\)/);
assert.match(String(err.message), /stop retrying/);
assert.match(String(err.message), /#2474/);
assert.match(String(err.message), /allowDshCompaction/);
Expand Down Expand Up @@ -131,7 +131,7 @@ test("A3: pipeline storm — first refusal warns once, then every DSH_REFUSAL_ST
assert.match(storm[0].msg, new RegExp(`refusal #${DSH_REFUSAL_STORM_LOG_EVERY}\\b`));
assert.match(storm[0].msg, /never shrinks \(#2490\)/);
assert.match(storm[0].msg, /#2474/);
assert.match(storm[0].msg, /allowDshCompaction/);
assert.match(storm[0].msg, /Disable dsh auto-compaction/);
assert.match(storm[0].msg, /#3013/);
} finally {
setLogCapture(null);
Expand Down Expand Up @@ -172,7 +172,7 @@ test("A3: passthrough storm — first-in-process warns, then the same cadence (n
if (firstInLoop) {
assert.equal(warns.length, 1, "process-first refusal warns");
assert.equal(infos.length, 1, "and the 100th logs one cadence info");
assert.match(infos[0].msg, /terminal; #3013/);
assert.match(infos[0].msg, /terminal, bili_final; #3013/);
} else {
assert.equal(infos.length, 1, "exactly one cadence info per 100 consecutive refusals");
}
Expand Down
7 changes: 5 additions & 2 deletions tests/preflight-client-abort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,17 @@ for (const pluginMode of [false, true]) {
test("cancelled growth auto-fold stops before fail-open forwarding and backoff", async () => {
await withUpstream(async (url, mainCalls) => {
const f = fixture(url, 50_000);
f.session.stats.lastInputTokens = 10_000;
// #3444 benefit gate: the growth arm needs decisionTrigger − target ≥
// AUTO_FOLD_MIN_BENEFIT_TOKENS (4096) — 10_000 vs 8192 left only 1808,
// so the trigger silently disarmed after 30ae2188a. 20_000 re-arms it.
f.session.stats.lastInputTokens = 20_000;
f.session.stats.lastInputTokensSource = "usage";
Object.assign(f.config, { externalSummary: parseExternalSummarySettings({
enabled: true, autoFold: true, autoFoldTargetTokens: 8192,
targets: [{ name: "sum", protocol: "responses", url, model: "test-model", apiKey: "fixture-key" }],
}, { inlineKeys: true }) });
f.prepared.nudge = f.core.processTurn({ messages: f.messages, state: f.session.state,
config: f.config, tokenCount: 10_000, renderTags: "text-only" }).nudge;
config: f.config, tokenCount: 20_000, renderTags: "text-only" }).nudge;
assert.ok(f.prepared.nudge?.compressibleRanges.length);
const processTurn = f.core.processTurn.bind(f.core);
f.core.processTurn = params => {
Expand Down
2 changes: 2 additions & 0 deletions tests/seq-ledger-pr3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ test("S12a: cloneSessionRows copies parent rows with identical seqs, lineage col
assert.equal(after.length, 7);
assert.equal(after[6]!.seq, 6, "child append lands at parent max seq + 1");
assert.equal(after[6]!.msgId, "g");
st.close(); // release the handle before test.after's rmSync — Windows cannot unlink an open index.db
});

test("S12b: partial branch (throughSeq) clones only the prefix", async () => {
Expand All @@ -108,6 +109,7 @@ test("S12b: partial branch (throughSeq) clones only the prefix", async () => {
const rows = st.getMessages("pr3-child-2");
assert.equal(rows.length, 3);
assert.deepEqual(rows.map((r) => r.msgId), ["a", "b", "c"]);
st.close(); // release the handle before test.after's rmSync — Windows cannot unlink an open index.db
});

test("S14: superseded-ref compress rejection — old refs refused with guidance, current refs pass", () => {
Expand Down
Loading