Files
gstack/test/ship-version-sync.test.ts
T
Garry Tan dcaea52800 v1.91.7.0 feat: add functional QA and pre-publication docs checks (#2983)
* feat: add surface-aware exploratory QA and ship documentation gates

* test: preserve delegated QA setup authority after main integration

* fix(qa): clarify exploration order and preserve report artifacts

* test(qa): follow the shared setup reference directly

* refactor(ship): make verification and recovery routes explicit

* test(ship): align evidence and review guards with explicit routes

* fix(workflows): clarify ship recovery and functional QA evidence

* fix(workflows): clarify approval recovery and full QA coverage

* refactor(workflows): order review transactions and clarify ship state

* fix(ship): clarify final verification and fail closed at publication

* fix(evals): attribute native atomic documentation writes

* fix(ship): clarify recovery and documentation lifecycle guidance

* fix(test): preserve observed native placeholder styling in CI

* fix(codex): report watchdog timeouts without a process-exit race

* Checkpoint functional QA implementation and workflow validation repairs

* Fix documentation and shared-review fixture contracts

* docs: clarify judge reuse and evaluation supervision

* test: align review evidence and selected case contracts

* test: verify append-only documentation checkpoints and recovery

* fix: qualify QA workflows and CI validation repairs

* fix: launch shared-libs fixture scripts on Windows

* fix: qualify QA deadlines, fixture isolation, and shard cleanup

* fix: preserve qualified QA and cancellation repairs

* fix: enforce functional fixture authority and share strict event decoding

* fix: retain free-test evidence and explain recovery

* fix: reject malformed native evidence after decoder consolidation

* test: use reliable capture for telemetry privacy filters

* test: refresh measured quick coverage and document validation costs

* Fix native fixture receipts and preserve VM validation evidence

* Align negative judge controls with upstream clarity policy

* Fix report-only QA preparation and public evidence handling

* Clarify QA-only preparation and current-report preservation

* Stream Ship quality judgments with an explicit 64k response contract

* Validate compact judge reasoning locally with supported wire schema

* Align functional QA fixture instructions with evidence acceptance

* Bind native browser diagnostics to execution evidence and align review verdicts

* Preserve native diagnostic line boundaries

* Serialize functional QA evidence from native captures

* Keep large QA evidence fixture payload out of Windows argv
2026-09-29 06:07:35 -07:00

256 lines
11 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;
test("rendered drift repair rejoins the existing-version queue check", () => {
const ship = readFileSync(join(import.meta.dir, '../ship/SKILL.md'), 'utf8');
const version = ship.slice(ship.indexOf('## Step 12:'), ship.indexOf('## Step 14:')).replace(/\s+/g, ' ');
expect(version).toMatch(/DRIFT_STALE_PKG[^\n]+repair[^\n]+reclassify[^\n]+ALREADY_BUMPED[^\n]+queue/);
expect(version).toContain('--current-version "$BASE_VERSION"');
expect(version).toContain('CANDIDATE_VERSION');
expect(version).toMatch(/keep[^\n]+NEW_VERSION[^\n]+currentVersion/);
});
test("rendered queue dispatch preserves offline git candidates without empty fallback fallthrough", () => {
const ship = readFileSync(join(import.meta.dir, '../ship/SKILL.md'), 'utf8');
const qualifyAt = ship.indexOf('**Qualify first:**');
const usableAt = ship.indexOf('**Usable candidate:**');
const missingAt = ship.indexOf('**No usable candidate:**');
expect(qualifyAt).toBeGreaterThanOrEqual(0);
expect(usableAt).toBeGreaterThan(qualifyAt);
expect(usableAt).toBeGreaterThanOrEqual(0);
expect(missingAt).toBeGreaterThan(usableAt);
const qualify = ship.slice(qualifyAt, usableAt).replace(/\s+/g, ' ');
const usable = ship.slice(usableAt, missingAt).replace(/\s+/g, ' ');
const missing = ship.slice(missingAt, ship.indexOf('4. **Write the bump**', missingAt)).replace(/\s+/g, ' ');
expect(qualify).toContain('require successful utility output and a nonempty valid version');
expect(qualify).toContain('`offline:false` qualifies; `offline:true` qualifies only with `fallback:"git"`');
expect(qualify).toContain('Offline output without that fallback, failure, malformed output or an empty version is unusable');
expect(usable).toContain('warnings and claimed queue');
expect(usable).toContain('CANDIDATE_VERSION');
expect(missing).toContain('local `BUMP_LEVEL` arithmetic');
expect(missing).toContain('ALREADY_BUMPED keeps `currentVersion`');
expect(missing).not.toContain('CANDIDATE_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");
});