Files
nextcraft/apps/cli/tests/guide-parity.test.ts
T
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00

98 lines
3.5 KiB
TypeScript

// guide ↔ runbook parity (v0.8 P5, G-210): the deploy guide's channel
// sections are the canonical text; `nextcraft guide` renders the same
// recipes. This test pins the AGREEMENT on every load-bearing marker —
// divergence is a test failure, never silent doc drift.
//
// It reads deploy/README.md (the runbook) and the rendered guide output,
// then asserts the markers that make the recipes work appear in BOTH:
// commands, flags, paths, ports, and the honest-limit notices. When
// either side changes a recipe, this test forces the other to follow.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { run } from "../src/index.ts";
import { testCtx } from "./helpers.ts";
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
// Markdown reflows (line-wraps split phrases) — parity compares on
// whitespace-normalized text so a wrap never fakes divergence.
const runbook = readFileSync(join(repoRoot, "deploy", "README.md"), "utf8")
.replace(/\s+/g, " ");
async function renderGuide(topic: string): Promise<string> {
const ctx = testCtx();
assert.equal(await run(["guide", topic], ctx), 0);
return ctx.out().replace(/\s+/g, " ");
}
// Markers that MUST appear in both the runbook and the guide per channel
// (commands, paths, ports — the things an admin copies).
const dockerMarkers = [
"docker run -d",
"-p 8420:8420",
"/home/nextcraft/.nextcraft",
"git.coreci.dev/coreci/nextcraft",
"seccomp=unconfined",
"SANDBOX FABRIC DEGRADED",
"docker pull",
];
const debMarkers = [
"dpkg -i",
"nodejs python3 git util-linux",
"deploy --service",
];
const binaryMarkers = [
"scripts/install.sh",
"aarch64",
"deploy --service",
];
const upgradeMarkers = [
"docker pull",
"dpkg -i",
"idempotent",
];
test("parity: guide docker topic agrees with the runbook's docker channel", async () => {
const guide = await renderGuide("docker");
for (const m of dockerMarkers) {
assert.ok(runbook.includes(m), `runbook carries: ${m}`);
assert.ok(guide.includes(m), `guide docker carries: ${m}`);
}
});
test("parity: guide deb topic agrees with the runbook's deb channel", async () => {
const guide = await renderGuide("deb");
for (const m of debMarkers) {
assert.ok(runbook.includes(m), `runbook carries: ${m}`);
assert.ok(guide.includes(m), `guide deb carries: ${m}`);
}
});
test("parity: guide binary topic agrees with the runbook's binary channel", async () => {
const guide = await renderGuide("binary");
for (const m of binaryMarkers) {
assert.ok(runbook.includes(m), `runbook carries: ${m}`);
assert.ok(guide.includes(m), `guide binary carries: ${m}`);
}
});
test("parity: guide upgrade topic agrees with the runbook's upgrade matrix", async () => {
const guide = await renderGuide("upgrade");
for (const m of upgradeMarkers) {
assert.ok(runbook.includes(m), `runbook carries: ${m}`);
assert.ok(guide.includes(m), `guide upgrade carries: ${m}`);
}
});
test("parity: every channel named in the runbook has a guide topic", async () => {
// The runbook's Pick-your-channel table names the three channels; the
// guide must carry exactly those recipes.
for (const channel of ["docker", "deb", "binary"]) {
assert.ok(runbook.includes(`**${channel}**`), `runbook names channel: ${channel}`);
const ctx = testCtx();
assert.equal(await run(["guide", channel], ctx), 0, `guide topic exists: ${channel}`);
}
});