fix(server): announce the port actually bound, not the port requested

`PORT=0` binds a kernel-assigned port but the startup line echoed the request,
printing ":0" — an address nothing listens on. The line is now the release
gate's only evidence of acceptance (see #93), so it has to be true: read the
bound address from the listener and print that. Covered by a new case in
tests/staging-health.test.js which serves /api/healthz on the announced port
and fails if the line still says ":0" (verified by reverting the log).
This commit is contained in:
timmy committed 2026-11-05 12:18:22 +00:00
1 parent bd4420b4d6
commit b2f95f8239
2 files changed
+26 -1

No files matched your search

+1 -1
View File
@@ -180,4 +180,4 @@ http.createServer(async(req,res)=>{
}
res.writeHead(200,{'content-type':types[extname(path)]||'application/octet-stream','cache-control':'no-store','x-content-type-options':'nosniff'});if(req.method==='HEAD')res.end();else res.end(body);
}catch(error){if(error instanceof AgentGatewayError)return sendAgentError(res,error);res.writeHead(404,{'content-type':'text/plain; charset=utf-8'});res.end('Not found')}
}).listen(port,host,()=>console.log(`Timmy is listening on http://${host}:${port} · vision ${visionConfig.enabled?'configured':'disabled'} · Hermes ${agentConfig.configured?'locked and ready':'disabled'}`));
}).listen(port,host,function(){const bound=this.address();console.log(`Timmy is listening on http://${host}:${bound.port} · vision ${visionConfig.enabled?'configured':'disabled'} · Hermes ${agentConfig.configured?'locked and ready':'disabled'}`)});
+25
View File
@@ -229,3 +229,28 @@ test('malformed and escaping base paths are rejected at startup', async () => {
assert.match(outcome.stderr, /TIMMY_BASE_PATH/);
}
});
// The startup line is the gate's only evidence of acceptance, so it has to name the port the process
// actually holds. It used to echo the requested port, which reads as ":0" for a kernel-assigned bind
// and sends anyone following it to a port nothing is listening on.
test('the startup line names the port actually bound, not the requested one', async t => {
const child = spawn(process.execPath, ['server.mjs'], {
cwd: root,
env: { ...process.env, PORT: '0', TIMMY_VISION_ENABLED: '0', TIMMY_AGENT_ENABLED: 'false' },
stdio: ['ignore', 'pipe', 'pipe'],
});
const outcome = await startupOutcome(child);
try {
assert.equal(outcome.listening, true, `the server must announce listening: ${outcome.stderr}`);
assert.doesNotMatch(outcome.stdout, / listening on http:\/\/[^:\s]+:0\b/);
const bound = /Timmy is listening on http:\/\/[^:]+:(\d+) ·/.exec(outcome.stdout);
assert.ok(bound, `the startup line must carry the bound port: ${outcome.stdout}`);
const response = await fetch(`http://127.0.0.1:${bound[1]}/api/healthz`);
assert.equal(response.status, 200);
assert.equal((await response.json()).ok, true);
} finally {
const closed = new Promise(resolve => child.once('close', resolve));
if (child.exitCode === null) child.kill('SIGTERM');
await closed;
}
});