fix: use production entrypoint for P1 manual serve
This commit is contained in:
@@ -72,7 +72,7 @@ test("prepare creates independent pending topology, fixtures, commands and guide
|
||||
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", "installation/runtime/p1-backend-supervisor.mjs", "GUIDE.md"]) await lstat(join(run.root, path));
|
||||
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", "logs/backend.log", "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; }
|
||||
@@ -102,19 +102,11 @@ test("cleanup removes only the exact stopped owned root and never creates verdic
|
||||
});
|
||||
|
||||
|
||||
async function installFakeServer(repo, { startupDelay = 0 } = {}) {
|
||||
async function installFakeServer(repo, { startupDelay = 0, healthStatus = 200, marker } = {}) {
|
||||
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}));});
|
||||
${marker ? `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(marker)}, "executed");` : ""}
|
||||
const server=http.createServer((req,res)=>{res.statusCode=req.url==="/health"?${healthStatus}:200;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}));});
|
||||
setTimeout(()=>server.listen(Number(process.env.PORT),process.env.HOST),${startupDelay});
|
||||
process.on("SIGTERM",()=>server.listening?server.close(()=>process.exit(0)):process.exit(0));
|
||||
`);
|
||||
await writeFile(join(repo,"backend/dist/config.js"),`export const loadConfig=env=>({host:env.HOST,port:Number(env.PORT)});
|
||||
`);
|
||||
await writeFile(join(repo,"backend/dist/app.js"),`import http from "node:http";
|
||||
export function buildApp(){let server;return{
|
||||
async listen({port,host}){await new Promise(r=>setTimeout(r,${startupDelay}));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}));});await new Promise((resolve,reject)=>{server.once("error",reject);server.listen(port,host,resolve);});},
|
||||
async close(){if(server?.listening)await new Promise((resolve,reject)=>server.close(error=>error?reject(error):resolve()));}
|
||||
};}
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -212,6 +204,14 @@ exec ${JSON.stringify(realGit)} "$@"
|
||||
}
|
||||
});
|
||||
|
||||
test("prepare records one regular 0600 backend log and no generated supervisor", async () => {
|
||||
const repo=await fakeRepo(),run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||
const owned=await readManualOwnership({repositoryRoot:repo}),entry=await lstat(join(run.root,"logs/backend.log"));
|
||||
assert.equal(entry.isFile(),true); assert.equal(entry.isSymbolicLink(),false); assert.equal(entry.mode&0o777,0o600);
|
||||
assert.deepEqual(owned.backendLog,{path:join(run.root,"logs/backend.log"),dev:entry.dev,ino:entry.ino});
|
||||
await assert.rejects(lstat(join(run.root,"installation/runtime/p1-backend-supervisor.mjs")));
|
||||
});
|
||||
|
||||
// A delayed real listener leaves the pre-fix port-check/spawn window open long enough for every
|
||||
// overlapping call. The backend.pid reservation, rather than scheduler timing, must pick one owner.
|
||||
test("concurrent serves reserve one exact process and leave no orphan after stop", { concurrency: false }, async () => {
|
||||
@@ -268,6 +268,52 @@ test("serve binds the one fixed loopback address, refuses a second PID, and guar
|
||||
await cleanupManual({repositoryRoot:repo});
|
||||
});
|
||||
|
||||
test("serve requires a 2xx HTTP health check and leaves no orphan on 503", { concurrency: false }, async () => {
|
||||
const repo=await fakeRepo(); await installFakeServer(repo,{healthStatus:503}); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||
const owned=await readManualOwnership({repositoryRoot:repo});
|
||||
await assert.rejects(serveManual({repositoryRoot:repo}),/health|readiness/i);
|
||||
await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||
assert.deepEqual(await listenerPids(),[]); assert.deepEqual(await matchingManualServerPids(run.root,owned.nonce),[]);
|
||||
});
|
||||
|
||||
test("serve launches exact server.js with immutable preload and fixed owned control port", { concurrency: false }, async () => {
|
||||
const repo=await fakeRepo(); await installFakeServer(repo); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||
const pid=await serveManual({repositoryRoot:repo}),record=JSON.parse(await readFile(join(run.root,"backend.pid"),"utf8"));
|
||||
assert.equal(record.pid,pid); assert.equal(record.script,join(repo,"backend/dist/server.js"));
|
||||
assert.equal(record.control.host,"127.0.0.1"); assert.equal(record.control.port,8792);
|
||||
assert.match(record.preload,/^data:text\/javascript;base64,/);
|
||||
const args=(await execFileAsync("ps",["-ww","-p",String(pid),"-o","command="])).stdout.trim();
|
||||
assert.equal(args,[process.execPath,"--import",record.preload,record.script,`--p1-manual-nonce=${record.nonce}`,`--p1-root=${run.root}`,`--p1-control-nonce=${record.reservationNonce}`].join(" "));
|
||||
await stopManual({repositoryRoot:repo});
|
||||
});
|
||||
|
||||
test("serve refuses legacy supervisor, runtime, server and log substitutions before code or outside writes", { concurrency: false }, async () => {
|
||||
for(const kind of ["legacy-supervisor","runtime-symlink","server-symlink","log-symlink","log-replaced"]){
|
||||
const repo=await fakeRepo(),marker=join(repo,`outside-${kind}.marker`); await installFakeServer(repo,{marker});
|
||||
const run=await prepareManual({repositoryRoot:repo,skipBuild:true}),outside=join(repo,`outside-${kind}`); await mkdir(outside);
|
||||
if(kind==="legacy-supervisor")await symlink(join(outside,"outside.mjs"),join(run.root,"installation/runtime/p1-backend-supervisor.mjs"));
|
||||
if(kind==="runtime-symlink"){await rm(join(run.root,"installation/runtime"),{recursive:true});await symlink(outside,join(run.root,"installation/runtime"));}
|
||||
if(kind==="server-symlink"){
|
||||
const external=join(outside,"server.js"); await writeFile(external,`import {writeFileSync} from "node:fs";writeFileSync(${JSON.stringify(marker)},"bad");`);
|
||||
await rm(join(repo,"backend/dist/server.js")); await symlink(external,join(repo,"backend/dist/server.js"));
|
||||
}
|
||||
if(kind==="log-symlink"){await rm(join(run.root,"logs/backend.log"));await symlink(join(outside,"captured.log"),join(run.root,"logs/backend.log"));}
|
||||
if(kind==="log-replaced"){await rm(join(run.root,"logs/backend.log"));await writeFile(join(run.root,"logs/backend.log"),"",{mode:0o600});}
|
||||
await assert.rejects(serveManual({repositoryRoot:repo}),/unsafe|identity|symlink|legacy|realpath|log/i,kind);
|
||||
await assert.rejects(lstat(marker),undefined,`${kind} must refuse before server execution`);
|
||||
assert.deepEqual(await readdir(outside),kind==="server-symlink"?["server.js"]:[]);
|
||||
await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||
await rm(run.root,{recursive:true,force:true});
|
||||
}
|
||||
});
|
||||
|
||||
test("serve refuses an occupied fixed control port before spawning", { concurrency: false }, async () => {
|
||||
const repo=await fakeRepo(),marker=join(repo,"server-executed"); await installFakeServer(repo,{marker}); const run=await prepareManual({repositoryRoot:repo,skipBuild:true});
|
||||
const blocker=net.createServer(); await new Promise((resolvePromise,reject)=>blocker.once("error",reject).listen(8792,"127.0.0.1",resolvePromise));
|
||||
try { await assert.rejects(serveManual({repositoryRoot:repo}),/8792.*occupied|control.*occupied/); } finally { await new Promise(resolvePromise=>blocker.close(resolvePromise)); }
|
||||
await assert.rejects(lstat(marker)); await assert.rejects(lstat(join(run.root,"backend.pid")));
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user