Files
gstack/test/ship-version-sync.test.ts
T
Garry TanandClaude Fable 5 3e674e4c01 fix: sweep — every sync spawn in the test trees carries a timeout (436 sites, 157 files)
spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process
per-test timeout can never fire while one waits — a hung child (stdin read,
network probe, dead daemon) wedges the whole shard until the runner's
external wall-clock SIGKILL. This exact class reached main: free-tests run
33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2
at the 360s wall while its five siblings finished in ~65s.

Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified
against its call site): default timeout: 30_000 (matches the free runner's
per-test budget), 120_000 for genuinely slow ops (installs, builds,
playwright, provider CLIs), helper wrappers fixed ONCE where call sites
route through them. Sites that only LOOK like calls (string fixtures, grep
needles, comments) were skipped with reasons — the enforcement commit that
follows marks them exempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 04:49:57 +00:00

225 lines
8.6 KiB
TypeScript

// /ship Step 12: VERSION ↔ package.json drift detection + repair.
// Mirrors the bash blocks in ship/SKILL.md.tmpl Step 12. When the template
// changes, update both sides together.
//
// Coverage gap: node-absent + bun-present path. Simulating "no node" in-process
// is flaky across dev machines; covered by manual spot-check + CI running on
// bun-only images if/when we add them.
import { test, expect, beforeEach, afterEach } from "bun:test";
import { execSync } from "node:child_process";
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "ship-drift-"));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
const writeFiles = (files: Record<string, string>) => {
for (const [name, content] of Object.entries(files)) {
writeFileSync(join(dir, name), content);
}
};
const pkgJson = (version: string | null, extra: Record<string, unknown> = {}) =>
JSON.stringify(
version === null ? { name: "x", ...extra } : { name: "x", version, ...extra },
null,
2,
) + "\n";
const idempotency = (base: string): { stdout: string; code: number } => {
const script = `
cd "${dir}" || exit 2
BASE_VERSION="${base}"
CURRENT_VERSION=$(cat VERSION 2>/dev/null | tr -d '\\r\\n[:space:]' || echo "0.0.0.0")
[ -z "$CURRENT_VERSION" ] && CURRENT_VERSION="0.0.0.0"
PKG_VERSION=""
PKG_EXISTS=0
if [ -f package.json ]; then
PKG_EXISTS=1
if command -v node >/dev/null 2>&1; then
PKG_VERSION=$(node -e 'const p=require("./package.json");process.stdout.write(p.version||"")' 2>/dev/null)
PARSE_EXIT=$?
elif command -v bun >/dev/null 2>&1; then
PKG_VERSION=$(bun -e 'const p=require("./package.json");process.stdout.write(p.version||"")' 2>/dev/null)
PARSE_EXIT=$?
else
echo "ERROR: no parser"; exit 1
fi
if [ "$PARSE_EXIT" != "0" ]; then
echo "ERROR: invalid JSON"; exit 1
fi
fi
if [ "$CURRENT_VERSION" = "$BASE_VERSION" ]; then
if [ "$PKG_EXISTS" = "1" ] && [ -n "$PKG_VERSION" ] && [ "$PKG_VERSION" != "$CURRENT_VERSION" ]; then
echo "STATE: DRIFT_UNEXPECTED"; exit 1
fi
echo "STATE: FRESH"
else
if [ "$PKG_EXISTS" = "1" ] && [ -n "$PKG_VERSION" ] && [ "$PKG_VERSION" != "$CURRENT_VERSION" ]; then
echo "STATE: DRIFT_STALE_PKG"
else
echo "STATE: ALREADY_BUMPED"
fi
fi`;
try {
const stdout = execSync(script, { shell: "/bin/bash", encoding: "utf8", timeout: 30_000 });
return { stdout: stdout.trim(), code: 0 };
} catch (e: any) {
return { stdout: (e.stdout || "").toString().trim(), code: e.status ?? 1 };
}
};
const bump = (newVer: string): { code: number } => {
const script = `
cd "${dir}" || exit 2
NEW_VERSION="${newVer}"
if ! printf '%s' "$NEW_VERSION" | grep -qE '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'; then
echo "invalid semver" >&2; exit 1
fi
echo "$NEW_VERSION" > VERSION
if [ -f package.json ]; then
node -e 'const fs=require("fs"),p=require("./package.json");p.version=process.argv[1];fs.writeFileSync("package.json",JSON.stringify(p,null,2)+"\\n")' "$NEW_VERSION"
fi`;
try {
execSync(script, { shell: "/bin/bash", stdio: "pipe", timeout: 30_000 });
return { code: 0 };
} catch (e: any) {
return { code: e.status ?? 1 };
}
};
const syncRepair = (): { code: number } => {
const script = `
cd "${dir}" || exit 2
REPAIR_VERSION=$(cat VERSION | tr -d '\\r\\n[:space:]')
if ! printf '%s' "$REPAIR_VERSION" | grep -qE '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'; then
echo "invalid repair semver" >&2; exit 1
fi
node -e 'const fs=require("fs"),p=require("./package.json");p.version=process.argv[1];fs.writeFileSync("package.json",JSON.stringify(p,null,2)+"\\n")' "$REPAIR_VERSION"`;
try {
execSync(script, { shell: "/bin/bash", stdio: "pipe", timeout: 30_000 });
return { code: 0 };
} catch (e: any) {
return { code: e.status ?? 1 };
}
};
const pkgVersion = () =>
JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).version;
// --- Idempotency classification: 6 cases ---
test("FRESH: VERSION == base, pkg synced", () => {
writeFiles({ VERSION: "0.0.0.0\n", "package.json": pkgJson("0.0.0.0") });
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: FRESH", code: 0 });
});
test("FRESH: VERSION == base, no package.json", () => {
writeFiles({ VERSION: "0.0.0.0\n" });
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: FRESH", code: 0 });
});
test("ALREADY_BUMPED: VERSION ahead, pkg synced", () => {
writeFiles({ VERSION: "0.1.0.0\n", "package.json": pkgJson("0.1.0.0") });
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: ALREADY_BUMPED", code: 0 });
});
test("ALREADY_BUMPED: VERSION ahead, no package.json", () => {
writeFiles({ VERSION: "0.1.0.0\n" });
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: ALREADY_BUMPED", code: 0 });
});
test("DRIFT_STALE_PKG: VERSION ahead, pkg stale", () => {
writeFiles({ VERSION: "0.1.0.0\n", "package.json": pkgJson("0.0.0.0") });
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: DRIFT_STALE_PKG", code: 0 });
});
test("DRIFT_UNEXPECTED: VERSION == base, pkg edited (exits non-zero)", () => {
writeFiles({ VERSION: "0.0.0.0\n", "package.json": pkgJson("0.5.0.0") });
const r = idempotency("0.0.0.0");
expect(r.stdout.startsWith("STATE: DRIFT_UNEXPECTED")).toBe(true);
expect(r.code).toBe(1);
});
// --- Parse failures: 2 cases ---
test("idempotency: invalid JSON exits non-zero with clear error", () => {
writeFiles({ VERSION: "0.1.0.0\n", "package.json": "{ not valid" });
const r = idempotency("0.0.0.0");
expect(r.code).toBe(1);
expect(r.stdout).toContain("invalid JSON");
});
test("idempotency: package.json with no version field treated as <none>", () => {
writeFiles({ VERSION: "0.1.0.0\n", "package.json": pkgJson(null) });
// PKG_VERSION is empty → drift check skipped → ALREADY_BUMPED
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: ALREADY_BUMPED", code: 0 });
});
// --- Bump: 3 cases ---
test("bump: writes VERSION and package.json in sync", () => {
writeFiles({ VERSION: "0.0.0.0\n", "package.json": pkgJson("0.0.0.0") });
expect(bump("0.1.0.0").code).toBe(0);
expect(readFileSync(join(dir, "VERSION"), "utf8").trim()).toBe("0.1.0.0");
expect(pkgVersion()).toBe("0.1.0.0");
});
test("bump: rejects invalid NEW_VERSION", () => {
writeFiles({ VERSION: "0.0.0.0\n", "package.json": pkgJson("0.0.0.0") });
const r = bump("not-a-version");
expect(r.code).toBe(1);
// VERSION is unchanged — validation runs before any write.
expect(readFileSync(join(dir, "VERSION"), "utf8").trim()).toBe("0.0.0.0");
});
test("bump: no package.json is silent", () => {
writeFiles({ VERSION: "0.0.0.0\n" });
expect(bump("0.1.0.0").code).toBe(0);
expect(readFileSync(join(dir, "VERSION"), "utf8").trim()).toBe("0.1.0.0");
expect(existsSync(join(dir, "package.json"))).toBe(false);
});
// --- Adversarial review regressions: trailing whitespace + invalid REPAIR_VERSION ---
test("trailing CR in VERSION does not cause false DRIFT_STALE_PKG", () => {
// Before the tr-strip fix, VERSION="0.1.0.0\r" read via cat would mismatch
// pkg.version="0.1.0.0" and classify as DRIFT_STALE_PKG, then repair would
// write garbage \r into package.json. Now CURRENT_VERSION is stripped.
writeFileSync(join(dir, "VERSION"), "0.1.0.0\r\n");
writeFileSync(join(dir, "package.json"), pkgJson("0.1.0.0"));
expect(idempotency("0.0.0.0")).toEqual({ stdout: "STATE: ALREADY_BUMPED", code: 0 });
});
test("DRIFT REPAIR rejects invalid VERSION semver instead of propagating", () => {
// If VERSION is corrupted/manually-edited to something non-semver, the
// repair path must refuse rather than writing junk into package.json.
writeFileSync(join(dir, "VERSION"), "not-a-semver\n");
writeFileSync(join(dir, "package.json"), pkgJson("0.0.0.0"));
const r = syncRepair();
expect(r.code).toBe(1);
// package.json must NOT have been overwritten with the garbage.
expect(pkgVersion()).toBe("0.0.0.0");
});
// --- THE critical regression test: drift-repair does NOT double-bump ---
test("DRIFT REPAIR: sync path syncs pkg to VERSION without re-bumping", () => {
// Simulate a prior /ship that bumped VERSION but failed to touch package.json.
writeFiles({ VERSION: "0.1.0.0\n", "package.json": pkgJson("0.0.0.0") });
// Idempotency classifies as DRIFT_STALE_PKG.
expect(idempotency("0.0.0.0").stdout).toBe("STATE: DRIFT_STALE_PKG");
// Sync-only repair runs — no re-bump.
expect(syncRepair().code).toBe(0);
// VERSION is unchanged. package.json now matches VERSION. No 0.2.0.0.
expect(readFileSync(join(dir, "VERSION"), "utf8").trim()).toBe("0.1.0.0");
expect(pkgVersion()).toBe("0.1.0.0");
});