136 lines
11 KiB
JavaScript
136 lines
11 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { chmod, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
import net from "node:net";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import test from "node:test";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
import {
|
|
cleanupManual, fixedManualRoot, prepareManual, readManualOwnership, serveManual, stopManual,
|
|
} from "./p1-manual-acceptance.mjs";
|
|
|
|
const roots = [];
|
|
async function fakeRepo() {
|
|
const root = await realpath(await mkdtemp(join(tmpdir(), "p1-manual-repo-")));
|
|
roots.push(root);
|
|
for (const path of ["scripts/p1-acceptance.sh", "scripts/test-p1-acceptance.sh", "backend/scripts/p1-acceptance.mjs", "backend/dist/server.js"]) {
|
|
await mkdir(dirname(join(root, path)), { recursive: true });
|
|
await writeFile(join(root, path), path.endsWith(".sh") ? "#!/bin/sh\n" : "export {};\n", { mode: 0o700 });
|
|
}
|
|
await mkdir(join(root, "harness", ".venv", "bin"), { recursive: true });
|
|
await writeFile(join(root, "harness", ".venv", "bin", "tht"), "#!/bin/sh\n", { mode: 0o700 });
|
|
await chmod(join(root, "harness", ".venv", "bin", "tht"), 0o700);
|
|
await mkdir(join(root, "harness", "workspaces"), { recursive: true });
|
|
return root;
|
|
}
|
|
test.afterEach(async () => Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))));
|
|
|
|
test("prepare refuses a pre-existing or symlink fixed root", async () => {
|
|
const repo = await fakeRepo(); const root = fixedManualRoot(repo);
|
|
await mkdir(root, { recursive: true });
|
|
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /already exists/);
|
|
await rm(root, { recursive: true });
|
|
const target = `${root}-target`; await mkdir(target, { recursive: true }); await symlink(target, root);
|
|
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /already exists|symlink/);
|
|
});
|
|
|
|
test("prepare requires Task 8 and prerequisites before creating state", async () => {
|
|
const repo = await fakeRepo(); await rm(join(repo, "scripts", "p1-acceptance.sh"));
|
|
await assert.rejects(prepareManual({ repositoryRoot: repo, skipBuild: true }), /Task 8/);
|
|
await assert.rejects(lstat(fixedManualRoot(repo)));
|
|
});
|
|
|
|
test("prepare creates independent pending topology, fixtures, commands and guide without verdict", async () => {
|
|
const repo = await fakeRepo(); const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
|
assert.equal(run.root, fixedManualRoot(repo));
|
|
const owned = await readManualOwnership({ repositoryRoot: repo });
|
|
assert.equal(owned.status, "PENDING"); assert.equal(owned.listener.host, "127.0.0.1"); assert.equal(owned.listener.port, 8791);
|
|
for (const path of ["remote.git/HEAD", "author/.git", "installation/registry", "fixture-secrets/dwh-password", "fixtures/descriptors/p1-filesystem.json", "requests/status.json", "responses", "exports", "rendered", "logs", "commands/render-1.sh", "commands/render-2.sh", "GUIDE.md"]) await lstat(join(run.root, path));
|
|
await assert.rejects(lstat(join(run.root, "VERDICT.md")));
|
|
const guide = await readFile(join(run.root, "GUIDE.md"), "utf8");
|
|
let previous = -1; for (let n = 1; n <= 14; n++) { const at = guide.indexOf(`${n}. `); assert.ok(at > previous, `step ${n} ordered`); previous = at; }
|
|
assert.doesNotMatch(guide, /cat .*fixture-secrets|show.*secret contents/i);
|
|
const traversal=JSON.parse(await readFile(join(run.root,"requests","invalid-traversal.json"),"utf8")); assert.match(traversal.workspace.evidence.source.uri,/\.\./);
|
|
const bindings=await readFile(join(run.root,"installation","bindings.env"),"utf8"); assert.match(bindings,new RegExp(`^THT_WORKSPACE_SECRET_ROOTS=.*fixture-secrets`,"m")); const scan=await readFile(join(run.root,"commands","secret-scan.sh"),"utf8"),extract=await readFile(join(run.root,"commands","extract-export.sh"),"utf8"); assert.match(scan,/rev-list/); assert.match(scan,/cat-file/); assert.match(scan,/installed-registry/); assert.match(extract,/ZIP contains a symlink or nonregular entry/);
|
|
const pubFs=await readFile(join(run.root,"commands","http-05-publish-p1-filesystem.sh"),"utf8"),pubHttp=await readFile(join(run.root,"commands","http-06-publish-p1-http.sh"),"utf8"),pubS3=await readFile(join(run.root,"commands","http-07-publish-p1-s3.sh"),"utf8"); assert.match(pubFs,/responses\/status\.json/); assert.match(pubHttp,/responses\/publish-p1-filesystem\.json/); assert.match(pubS3,/responses\/publish-p1-http\.json/); assert.doesNotMatch(pubFs,/REPLACE_WITH/);
|
|
const render = await readFile(join(run.root, "commands", "render-1.sh"), "utf8");
|
|
for(const name of await readdir(join(run.root,"commands")))if(name.endsWith(".sh"))await execFileAsync("bash",["-n",join(run.root,"commands",name)]);
|
|
assert.match(render, /read-p1-filesystem\.json/); assert.match(render, /responses\/pull\.json/); assert.doesNotMatch(render, /responses\/publish-p1-filesystem\.json/); assert.match(render, /snapshotPath/); assert.match(render, /p1-render-snapshot\.mjs/);
|
|
});
|
|
|
|
test("cleanup rejects unowned, live, mismatched and symlink state and preserves siblings", async () => {
|
|
const repo = await fakeRepo(); const integration = join(repo, ".artifacts", "p1-integration"); const sibling = join(repo, ".artifacts", "manual-acceptance", "foreign");
|
|
await mkdir(integration, { recursive: true }); await writeFile(join(integration, "sentinel"), "keep");
|
|
await mkdir(sibling, { recursive: true }); await writeFile(join(sibling, "sentinel"), "keep");
|
|
await assert.rejects(cleanupManual({ repositoryRoot: repo }), /ownership|root/);
|
|
const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
|
const ownershipPath = join(run.root, "ownership.json"); const owned = JSON.parse(await readFile(ownershipPath)); owned.root += "-wrong"; await writeFile(ownershipPath, JSON.stringify(owned));
|
|
await assert.rejects(cleanupManual({ repositoryRoot: repo }), /identity/); assert.equal((await lstat(run.root)).isDirectory(), true);
|
|
assert.equal(await readFile(join(integration, "sentinel"), "utf8"), "keep"); assert.equal(await readFile(join(sibling, "sentinel"), "utf8"), "keep");
|
|
});
|
|
|
|
test("cleanup removes only the exact stopped owned root and never creates verdict", async () => {
|
|
const repo = await fakeRepo(); const run = await prepareManual({ repositoryRoot: repo, skipBuild: true });
|
|
await cleanupManual({ repositoryRoot: repo }); await assert.rejects(lstat(run.root));
|
|
});
|
|
|
|
|
|
async function installFakeServer(repo) {
|
|
await writeFile(join(repo, "backend", "dist", "server.js"), `import http from "node:http";
|
|
const server=http.createServer((req,res)=>{res.setHeader("content-type","application/json");res.end(JSON.stringify({status:"ok",ambient:process.env.THT_DWH_API_KEY,maintenance:process.env.MAINTENANCE_STATE_FILE,wrongMaintenance:process.env.THT_MAINTENANCE_STATE_FILE}));});
|
|
server.listen(Number(process.env.PORT),process.env.HOST);
|
|
process.on("SIGTERM",()=>server.close(()=>process.exit(0)));
|
|
`);
|
|
}
|
|
|
|
test("serve binds the one fixed loopback address, refuses a second PID, and guarded stop removes identity", { concurrency: false }, async () => {
|
|
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
|
const priorAmbient=process.env.THT_DWH_API_KEY; process.env.THT_DWH_API_KEY="AMBIENT-MUST-NOT-PASS"; const pid=await serveManual({repositoryRoot:repo}); assert.equal(Number.isSafeInteger(pid),true);
|
|
const health=await (await fetch("http://127.0.0.1:8791/health")).json(); assert.equal(health.status,"ok"); assert.equal(health.ambient,undefined); assert.equal(health.wrongMaintenance,undefined); assert.equal(health.maintenance,join(run.root,"installation/data/maintenance.json")); if(priorAmbient===undefined)delete process.env.THT_DWH_API_KEY;else process.env.THT_DWH_API_KEY=priorAmbient;
|
|
await assert.rejects(serveManual({repositoryRoot:repo}),/PID record/);
|
|
await stopManual({repositoryRoot:repo}); await assert.rejects(lstat(join(run.root,"backend.pid")));
|
|
await assert.rejects(fetch("http://127.0.0.1:8791/health",{signal:AbortSignal.timeout(200)}));
|
|
await cleanupManual({repositoryRoot:repo});
|
|
});
|
|
|
|
test("serve refuses an occupied fixed port and never creates a PID or verdict", { concurrency: false }, async () => {
|
|
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
|
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8791,"127.0.0.1",resolvePromise));
|
|
try { await assert.rejects(serveManual({repositoryRoot:repo}),/occupied/); } finally { await new Promise(resolvePromise=>blocker.close(resolvePromise)); }
|
|
await assert.rejects(lstat(join(run.root,"backend.pid"))); await assert.rejects(lstat(join(run.root,"VERDICT.md")));
|
|
});
|
|
|
|
test("serve and cleanup refuse stale or mismatched PID records without signaling", async () => {
|
|
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
|
await writeFile(join(run.root,"backend.pid"),JSON.stringify({pid:999999,nonce:"wrong"}));
|
|
await assert.rejects(serveManual({repositoryRoot:repo}),/PID record/);
|
|
await assert.rejects(stopManual({repositoryRoot:repo}),/identity mismatch/);
|
|
await assert.rejects(cleanupManual({repositoryRoot:repo}),/identity|stale/);
|
|
assert.equal((await lstat(run.root)).isDirectory(),true);
|
|
});
|
|
|
|
test("generated render command validates saved responses and owned snapshot before renderer", async () => {
|
|
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}); const script=join(run.root,"commands/render-1.sh"), output=join(run.root,"rendered/runtime-1.yaml");
|
|
const invoke=()=>execFileAsync("bash",[script],{cwd:repo});
|
|
await assert.rejects(invoke(),/saved response is missing/);
|
|
await writeFile(join(run.root,"responses/read-p1-filesystem.json"),"{"); await writeFile(join(run.root,"responses/pull.json"),"{}");
|
|
await assert.rejects(invoke(),/malformed JSON/);
|
|
const a="a".repeat(40),b="b".repeat(40),outside=join(repo,"outside.yaml"); await writeFile(outside,"x");
|
|
await writeFile(join(run.root,"responses/read-p1-filesystem.json"),JSON.stringify({revision:{commit:a,snapshotPath:outside}})); await writeFile(join(run.root,"responses/pull.json"),JSON.stringify({head:b}));
|
|
await assert.rejects(invoke(),/revisions differ/);
|
|
await writeFile(join(run.root,"responses/pull.json"),JSON.stringify({head:a})); await assert.rejects(invoke(),/snapshot escapes/);
|
|
await assert.rejects(lstat(output));
|
|
});
|
|
|
|
|
|
test("generated secret scan checks reachable Git blobs without printing contents", async () => {
|
|
const repo=await fakeRepo(); const run=await prepareManual({repositoryRoot:repo,skipBuild:true}); const installed=join(run.root,"installation/registry/repo"),author=join(run.root,"author"),scan=join(run.root,"commands/secret-scan.sh");
|
|
await execFileAsync("git",["clone",join(run.root,"remote.git"),installed]); await execFileAsync("bash",[scan],{cwd:repo});
|
|
const canary="DWH-"+"c".repeat(32); await writeFile(join(author,"temporary-secret"),canary); await execFileAsync("git",["add","temporary-secret"],{cwd:author}); await execFileAsync("git",["commit","-m","temporary canary"],{cwd:author}); await execFileAsync("git",["rm","temporary-secret"],{cwd:author}); await execFileAsync("git",["commit","-m","remove canary"],{cwd:author});
|
|
await assert.rejects(execFileAsync("bash",[scan],{cwd:repo}),error=>/reachable Git blob/.test(error.stderr)&&!error.stderr.includes(canary));
|
|
});
|