fix(test): decide base-path rejection from the server's own startup line (#93)
Quality gates / quality (pull_request) Waiting to run
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:
1 parent
58c6d86b66
commit
f334fe9d20
1 file changed
+35
-10
@@ -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/);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user