mirror of
https://github.com/CharlesWiltgen/Axiom.git
synced 2026-09-20 19:58:20 +08:00
73bcab39b0
set-version.js regenerated the Cursor variant but never Codex, so axiom-codex/.codex-plugin/plugin.json kept the PREVIOUS version through a version bump — invisibly. Gate 12f compares skills/ and agents/ mtimes against that manifest and a pure version bump touches neither, while the version-parity gate (check 8) covered Cursor's manifest but not Codex's. Confirmed by reverting the manifest to the prior version and watching Phase 1 pass green. Root package.json had the identical hole: written by set-version.js, but read by pre-deploy only for the `pi` manifest block, never into the parity map. Also confirmed empirically. `pi install` resolves against that file. - set-version.js now runs build-cursor.ts AND build-codex.ts - --tag preflight absolves axiom-codex/ output via a new codex-output.js, mirroring cursor-output.js (regression the fix itself introduced: the preflight would have refused on ~350 files the same run had just written) - check 8 reads every version carrier from one table; parse wrapped in try/catch because build-codex rmSync's and rebuilds in place with no staging - version-parity.test.ts fails if a carrier is added without a matching read - methodology-leak.test.ts keeps behavioral-test methodology out of .claude/rules/skill-development.md, which the harness appends to every skill-file read and therefore reaches a test's GREEN arm but never its control Parity gate now reports 9 files, up from 7. Unit suite 488/488 (was 478). Reviewed with code-reviewer; the root package.json hole was its finding.
114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
/**
|
|
* Every file in the repo that carries the Axiom plugin version.
|
|
*
|
|
* This list is the CLASS, and it exists because the class kept leaking one
|
|
* member at a time. Two independent instances were found on 2026-09-05, both
|
|
* fully green under the whole gate suite while desynced:
|
|
*
|
|
* - `axiom-codex/.codex-plugin/plugin.json` — written only by build-codex.ts,
|
|
* which set-version.js did not run. Gate 12f (Codex staleness) compares
|
|
* skill/agent mtimes against the manifest and a version bump touches
|
|
* neither, so it could not fire.
|
|
* - `package.json` (root) — written by set-version.js, but pre-deploy read it
|
|
* only for the `pi` manifest block, never into the version-parity map.
|
|
* `pi install git:github.com/CharlesWiltgen/Axiom` resolves against it.
|
|
*
|
|
* A file that carries the version but is absent from pre-deploy's check 8 is
|
|
* invisible to every gate. This test fails when the two drift.
|
|
*/
|
|
const VERSION_CARRYING_FILES = [
|
|
".claude-plugin/plugins/axiom/claude-code.json",
|
|
".claude-plugin/plugins/axiom/.claude-plugin/plugin.json",
|
|
".claude-plugin/marketplace.json",
|
|
".claude-plugin/plugins/axiom/hooks/metadata.txt",
|
|
"docs/.vitepress/config.ts",
|
|
"axiom-mcp/package.json",
|
|
"axiom-cursor/.cursor-plugin/plugin.json",
|
|
"axiom-codex/.codex-plugin/plugin.json",
|
|
"package.json",
|
|
] as const;
|
|
|
|
function canonicalVersion(): string {
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(root, ".claude-plugin/plugins/axiom/claude-code.json"), "utf8"),
|
|
);
|
|
return manifest.version;
|
|
}
|
|
|
|
/** Read the version out of a file, whatever shape it stores it in. */
|
|
function versionOf(relPath: string): string | undefined {
|
|
const abs = path.join(root, relPath);
|
|
if (!fs.existsSync(abs)) return undefined;
|
|
const raw = fs.readFileSync(abs, "utf8");
|
|
|
|
if (relPath.endsWith("metadata.txt")) return raw.trim().split("\n")[0];
|
|
if (relPath.endsWith("config.ts")) return raw.match(/• v([0-9][^\s"'`<]*)/)?.[1];
|
|
if (relPath === ".claude-plugin/marketplace.json") {
|
|
return JSON.parse(raw).plugins?.find((p: { name: string }) => p.name === "axiom")?.version;
|
|
}
|
|
return JSON.parse(raw).version;
|
|
}
|
|
|
|
test("every version-carrying file exists", () => {
|
|
for (const relPath of VERSION_CARRYING_FILES) {
|
|
assert.ok(
|
|
fs.existsSync(path.join(root, relPath)),
|
|
`${relPath} is missing — regenerate the variant that owns it`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("every version-carrying file matches the canonical version", () => {
|
|
const canonical = canonicalVersion();
|
|
for (const relPath of VERSION_CARRYING_FILES) {
|
|
assert.equal(
|
|
versionOf(relPath),
|
|
canonical,
|
|
`${relPath} is out of sync with claude-code.json (${canonical})`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("pre-deploy check 8 covers exactly the version-carrying files", () => {
|
|
// The guard that actually closes the class: a carrier added to the list above
|
|
// without a matching read in check 8 — or dropped from check 8 — fails here
|
|
// rather than shipping green.
|
|
//
|
|
// Check 8 populates `versions` two ways, so count both: direct literal-key
|
|
// assignments (`versions["claude-code.json"] = …`, which use short display
|
|
// labels rather than repo paths, so a path-substring match cannot see them),
|
|
// plus the VERSION_CARRYING_FILES table it loops over.
|
|
const preDeploy = fs.readFileSync(path.join(root, "scripts/pre-deploy.ts"), "utf8");
|
|
|
|
const directKeys = [...preDeploy.matchAll(/\bversions\[\s*"([^"]+)"\s*\]\s*=/g)].map((m) => m[1]);
|
|
const tableBlock = preDeploy.match(
|
|
/const VERSION_CARRYING_FILES[\s\S]*?\n\];/,
|
|
)?.[0];
|
|
assert.ok(tableBlock, "pre-deploy.ts no longer declares VERSION_CARRYING_FILES");
|
|
const tableEntries = [...tableBlock.matchAll(/\[\s*"[^"]+",\s*"([^"]+)"/g)].map((m) => m[1]);
|
|
|
|
const covered = new Set([...directKeys, ...tableEntries]);
|
|
assert.equal(
|
|
covered.size,
|
|
VERSION_CARRYING_FILES.length,
|
|
`check 8 reads ${covered.size} version carriers but this test lists ` +
|
|
`${VERSION_CARRYING_FILES.length}. Covered: ${[...covered].sort().join(", ")}`,
|
|
);
|
|
|
|
// The table half must match by real repo path; the direct half uses labels.
|
|
for (const relPath of tableEntries) {
|
|
assert.ok(
|
|
(VERSION_CARRYING_FILES as readonly string[]).includes(relPath),
|
|
`check 8 reads ${relPath}, which this test does not list as a version carrier`,
|
|
);
|
|
}
|
|
});
|