diff --git a/lib/ship-queue-train-cli.ts b/lib/ship-queue-train-cli.ts index a9e779d15..01fb58477 100644 --- a/lib/ship-queue-train-cli.ts +++ b/lib/ship-queue-train-cli.ts @@ -118,7 +118,9 @@ export async function trainMain(ctx: QueueContext, args: string[]): Promise 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') })); diff --git a/test/ship-queue-train-ops.test.ts b/test/ship-queue-train-ops.test.ts index d82bc5f4c..d1b39013b 100644 --- a/test/ship-queue-train-ops.test.ts +++ b/test/ship-queue-train-ops.test.ts @@ -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); });