Skip to content

Commit eaf0585

Browse files
authored
fix(flaky): propagate auth trigger errors (#217)
1 parent 45dcd08 commit eaf0585

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

src/commands/test.flaky.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { runFlaky } from './test.js';
1818

1919
type FetchInput = Parameters<typeof globalThis.fetch>[0];
2020
type RunStatus = 'passed' | 'failed' | 'blocked' | 'cancelled';
21+
type TriggerAuthErrorCode = 'AUTH_REQUIRED' | 'AUTH_INVALID' | 'AUTH_FORBIDDEN';
2122

2223
function urlOf(input: FetchInput): string {
2324
return typeof input === 'string'
@@ -46,6 +47,7 @@ function makeFlakyFetch(opts: {
4647
statuses: RunStatus[];
4748
testType?: 'frontend' | 'backend';
4849
notFoundOnTrigger?: boolean;
50+
triggerAuthError?: TriggerAuthErrorCode;
4951
}): { fetchImpl: FetchImpl; triggerCount: () => number } {
5052
let triggers = 0;
5153
const testType = opts.testType ?? 'frontend';
@@ -67,6 +69,17 @@ function makeFlakyFetch(opts: {
6769
}
6870

6971
if (method === 'POST' && url.includes('/runs/rerun')) {
72+
if (opts.triggerAuthError) {
73+
return jsonResponse(opts.triggerAuthError === 'AUTH_FORBIDDEN' ? 403 : 401, {
74+
error: {
75+
code: opts.triggerAuthError,
76+
message: 'auth failed',
77+
nextAction: 'run setup',
78+
requestId: 'req_auth',
79+
details: {},
80+
},
81+
});
82+
}
7083
if (opts.notFoundOnTrigger) {
7184
return jsonResponse(404, {
7285
error: {
@@ -345,6 +358,35 @@ describe('runFlaky', () => {
345358
expect((err as ApiError).code).toBe('NOT_FOUND');
346359
});
347360

361+
it.each(['AUTH_REQUIRED', 'AUTH_INVALID', 'AUTH_FORBIDDEN'] as const)(
362+
'propagates %s during trigger instead of scoring an error attempt',
363+
async code => {
364+
const { fetchImpl, triggerCount } = makeFlakyFetch({
365+
statuses: [],
366+
triggerAuthError: code,
367+
});
368+
const { deps, stdout } = makeDeps(fetchImpl);
369+
const err = await runFlaky(
370+
{
371+
profile: 'default',
372+
output: 'json',
373+
dryRun: false,
374+
debug: false,
375+
verbose: false,
376+
testId: 'test_x',
377+
runs: 3,
378+
untilFail: false,
379+
timeoutSeconds: 600,
380+
},
381+
deps,
382+
).catch((e: unknown) => e);
383+
expect(err).toBeInstanceOf(ApiError);
384+
expect(err).toMatchObject({ code, exitCode: 3 });
385+
expect(stdout).toEqual([]);
386+
expect(triggerCount()).toBe(0);
387+
},
388+
);
389+
348390
it('rejects --runs below the range (0) with a validation error (exit 5)', async () => {
349391
const { fetchImpl } = makeFlakyFetch({ statuses: [] });
350392
const { deps } = makeDeps(fetchImpl);

src/commands/test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9379,6 +9379,18 @@ const MAX_FLAKY_RUNS = 10;
93799379
/** Default replay count when `--runs` is omitted. */
93809380
const DEFAULT_FLAKY_RUNS = 5;
93819381

9382+
function isFlakyFatalTriggerError(err: unknown): boolean {
9383+
if (!(err instanceof ApiError)) return false;
9384+
switch (err.code) {
9385+
case 'AUTH_REQUIRED':
9386+
case 'AUTH_INVALID':
9387+
case 'AUTH_FORBIDDEN':
9388+
return true;
9389+
default:
9390+
return false;
9391+
}
9392+
}
9393+
93829394
interface RunTestFlakyOptions extends CommonOptions {
93839395
testId: string;
93849396
/** Number of replays to run (1..MAX_FLAKY_RUNS). */
@@ -9477,6 +9489,9 @@ export async function runFlaky(
94779489
},
94789490
});
94799491
}
9492+
if (isFlakyFatalTriggerError(err)) {
9493+
throw err;
9494+
}
94809495
// Any other trigger error is recorded as an errored attempt so a single
94819496
// transient blip doesn't abort a long stability probe.
94829497
const code = err instanceof ApiError ? err.code : 'ERROR';

0 commit comments

Comments
 (0)