Skip to content

Commit 6d9eb7c

Browse files
committed
fix(flaky): propagate auth trigger errors
1 parent b9e9601 commit 6d9eb7c

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
@@ -8369,6 +8369,18 @@ const MAX_FLAKY_RUNS = 10;
83698369
/** Default replay count when `--runs` is omitted. */
83708370
const DEFAULT_FLAKY_RUNS = 5;
83718371

8372+
function isFlakyFatalTriggerError(err: unknown): boolean {
8373+
if (!(err instanceof ApiError)) return false;
8374+
switch (err.code) {
8375+
case 'AUTH_REQUIRED':
8376+
case 'AUTH_INVALID':
8377+
case 'AUTH_FORBIDDEN':
8378+
return true;
8379+
default:
8380+
return false;
8381+
}
8382+
}
8383+
83728384
interface RunTestFlakyOptions extends CommonOptions {
83738385
testId: string;
83748386
/** Number of replays to run (1..MAX_FLAKY_RUNS). */
@@ -8467,6 +8479,9 @@ export async function runFlaky(
84678479
},
84688480
});
84698481
}
8482+
if (isFlakyFatalTriggerError(err)) {
8483+
throw err;
8484+
}
84708485
// Any other trigger error is recorded as an errored attempt so a single
84718486
// transient blip doesn't abort a long stability probe.
84728487
const code = err instanceof ApiError ? err.code : 'ERROR';

0 commit comments

Comments
 (0)