fix(test): decide base-path rejection from the server's own startup line (#93)
Quality gates / quality (pull_request) Waiting to run

The old gate raced child exit against a fixed 800 ms timer. Under full-suite
CPU contention the child was still importing modules, so it was killed and
the timeout was asserted as acceptance: 2/2 full-suite runs on main failed
with '"timmy-staging" was accepted' while the file alone passed 3/3 and the
server rejected the value directly.

Acceptance is now decided by the child printing 'Timmy is listening on
http://'; a child that neither exits nor listens inside 20 s is reported as
indeterminate. Rejection assertions (non-zero exit, TIMMY_BASE_PATH in
stderr) still run.

Mutation check: with the leading-slash validation disabled the repaired
test fails in 398 ms with the acceptance message.
This commit is contained in:
Timmy committed 2026-11-05 10:59:23 +00:00
1 parent 58c6d86b66
commit f334fe9d20
1 file changed
+35 -10
+35 -10
View File
@@ -125,6 +125,30 @@ test('agent cookie is constrained to the normalized base path', async t => {
assert.match(response.headers.get('set-cookie'), /; Path=\/timmy-staging\//);
});
// The child's own startup line is the only trustworthy evidence that a base path was accepted.
// A fixed timer cannot tell "still importing modules" from "server is up": on a loaded host the
// old 800 ms race killed a correctly-rejecting child and reported it as accepted.
function startupOutcome(child, deadlineMs = 20_000) {
return new Promise(resolve => {
let stdout = '';
let stderr = '';
let settled = false;
const finish = outcome => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({ ...outcome, stdout, stderr });
};
const timer = setTimeout(() => finish({ timedOut: true }), deadlineMs);
child.stdout.on('data', chunk => {
stdout += chunk;
if (/^Timmy is listening on http:\/\//m.test(stdout)) finish({ listening: true });
});
child.stderr.on('data', chunk => { stderr += chunk; });
child.once('exit', code => finish({ code }));
});
}
test('malformed and escaping base paths are rejected at startup', async () => {
const invalid = [
'timmy-staging',
@@ -152,16 +176,17 @@ test('malformed and escaping base paths are rejected at startup', async () => {
const child = spawn(process.execPath, ['server.mjs'], {
cwd: root,
env: { ...process.env, PORT: '0', TIMMY_BASE_PATH: value },
stdio: ['ignore', 'ignore', 'pipe'],
stdio: ['ignore', 'pipe', 'pipe'],
});
let stderr = '';
child.stderr.on('data', chunk => { stderr += chunk; });
const exitCode = await Promise.race([
new Promise(resolve => child.once('exit', resolve)),
new Promise(resolve => setTimeout(() => { child.kill('SIGTERM'); resolve('timeout'); }, 800)),
]);
assert.notEqual(exitCode, 'timeout', `${JSON.stringify(value)} was accepted`);
assert.notEqual(exitCode, 0, `${JSON.stringify(value)} exited successfully`);
assert.match(stderr, /TIMMY_BASE_PATH/);
const outcome = await startupOutcome(child);
if (outcome.listening) {
const closed = new Promise(resolve => child.once('close', resolve));
child.kill('SIGKILL');
await closed;
}
assert.equal(outcome.listening, undefined, `${JSON.stringify(value)} was accepted: the server announced it was listening`);
assert.notEqual(outcome.timedOut, true, `${JSON.stringify(value)} neither exited nor announced listening within the startup deadline`);
assert.notEqual(outcome.code, 0, `${JSON.stringify(value)} exited successfully`);
assert.match(outcome.stderr, /TIMMY_BASE_PATH/);
}
});