train watch: reject a malformed --free-timeout as usage instead of taking the default

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 16:12:57 +00:00
1 parent 5ec4667a13
commit db7c8c8940
2 files changed
+5 -2

No files matched your search

+3 -1
View File
@@ -118,7 +118,9 @@ export async function trainMain(ctx: QueueContext, args: string[]): Promise<numb
if (sub === 'watch') {
const timeoutSec = positive(flag(rest, '--timeout'), 7200);
if (Number.isNaN(timeoutSec)) return usage('--timeout needs seconds');
return guarded('train watch', () => trainWatch(ctx, { train: nonce, timeoutSec, pollMs: Number(process.env.GSTACK_SHIP_QUEUE_POLL_MS) || 30_000, freeTimeoutSec: positive(flag(rest, '--free-timeout'), 2700) || 2700 }));
const freeTimeoutSec = positive(flag(rest, '--free-timeout'), 2700);
if (Number.isNaN(freeTimeoutSec)) return usage('--free-timeout needs seconds');
return guarded('train watch', () => trainWatch(ctx, { train: nonce, timeoutSec, pollMs: Number(process.env.GSTACK_SHIP_QUEUE_POLL_MS) || 30_000, freeTimeoutSec }));
}
if (sub === 'verdict') return guarded('train verdict', () => trainVerdict(ctx, { train: nonce }));
if (sub === 'rebuild') return guarded('train rebuild', () => trainRebuild(ctx, { train: nonce, infra: rest.includes('--infra') }));
+2 -1
View File
@@ -275,7 +275,7 @@ describe('train prune --stale and train watch', () => {
}, 120_000);
// Value: protects=a restarted controller re-arms with one command: watch continues from the record (free tests, gate dispatch, the pending run, the verdict) and prints NEXT;
// fails_when=watch dispatches twice, blocks past --timeout, or does not reach the verdict after a restart; why_new=no resumable follow existed (hand-run gh run watch chains died with the machine); seam=GSTACK_SHIP_QUEUE_POLL_MS; no_claim=real GitHub run timing
// fails_when=watch dispatches twice, blocks past --timeout, silently replaces a malformed --free-timeout with the default, or does not reach the verdict after a restart; why_new=no resumable follow existed (hand-run gh run watch chains died with the machine); seam=GSTACK_SHIP_QUEUE_POLL_MS; no_claim=real GitHub run timing
test('watch resumes from free-pending through the gate dispatch to the verdict, times out with its own NEXT, and never dispatches twice', () => {
const fx = make();
fx.write(d => { d.rules['free-tests.yml'] = { status: 'in_progress', conclusion: null }; });
@@ -307,6 +307,7 @@ describe('train prune --stale and train watch', () => {
expect([idle.status, line(idle.out, 'WATCH: nothing')]).toEqual([EXIT.ok, 'WATCH: nothing to wait for in state green']);
expect(next(idle.out)).toBe(`NEXT: gstack-ship-queue verify --pr 1 --train ${nonce}`);
expect(queue(fx, ['train', 'watch']).status).toBe(EXIT.usage);
expect(queue(fx, ['train', 'watch', '--train', nonce, '--free-timeout', 'abc']).status).toBe(EXIT.usage);
expect(queue(fx, ['train', 'watch', '--train', 'no-such-train']).err).toMatch(/no train record no-such-train.*\(QUEUE_PROOF_MISSING\)/);
}, 120_000);
});