mirror of
https://github.com/przeprogramowani/10x-cli.git
synced 2026-09-19 03:30:01 +08:00
a68c743578
Wrap the auth-guard refresh path in a proper-lockfile lock that re-reads
auth.json inside the critical section, so cooperating CLI processes (and
parallel in-process callers) racing on a near-expiry token can no longer
double-refresh — the late caller observes the rotated token and short-
circuits. Lock policy: 5 retries with 100–1000ms exponential backoff and
a 10s stale threshold, with auth_lock_timeout surfaced as a clean error
envelope on contention.
Add three test suites locking in invariants previously enforced only by
convention:
- auth-guard-concurrency: in-process race, cross-process race via
child_process.fork, stale-lock recovery, contention timeout
- exit-codes: per-command exit-code matrix for auth login/--status
/--logout, including the F1 fix (expired token in JSON mode → exit 3)
- json-envelope: stdout envelope contract + leakage guard catching
stray verbose markers, ANSI escapes, clack glyphs, multi-line output,
and accidental email echo
Share auth-flow and @clack/prompts module mocks via tests/helpers/* so
mock.module registrations don't leak across test files in the same
bun test process; the mocks fall through to the real implementations
when no test state is configured, leaving auth-flow.test.ts untouched.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
/**
|
|
* Shared module mock for src/lib/auth-flow.
|
|
*
|
|
* Cross-file pollution problem: bun loads ALL test files (and their
|
|
* top-level mock.module calls) before running any test. If two test files
|
|
* each call mock.module("../src/lib/auth-flow", ...), the LATTER wins and
|
|
* its closures dominate — even for unrelated test files like
|
|
* auth-flow.test.ts that just want the real implementation.
|
|
*
|
|
* This helper sidesteps the issue by:
|
|
* 1. Importing the real auth-flow module BEFORE installing the mock and
|
|
* capturing the function references in stable closures.
|
|
* 2. Installing the mock exactly ONCE here. Both exit-codes.test.ts and
|
|
* json-envelope.test.ts depend on this helper, so they share a single
|
|
* mock registration.
|
|
* 3. Falling through to the real implementation when the test state is
|
|
* not configured, so unrelated tests (auth-flow.test.ts) keep working
|
|
* against globalThis.fetch as they did before.
|
|
*
|
|
* Tests opt in by assigning to authFlowMockState in their beforeEach hook
|
|
* and resetting to null in afterEach to leave the slate clean for any test
|
|
* file that runs after them.
|
|
*/
|
|
|
|
import { mock } from "bun:test";
|
|
import type { ApiResult } from "../../src/lib/api-client";
|
|
import type { LoginResponse, PollResult, TokenBundle } from "../../src/lib/auth-flow";
|
|
|
|
// Capture the real module BEFORE installing the mock.
|
|
const real = await import("../../src/lib/auth-flow");
|
|
const realLogin = real.loginRequest;
|
|
const realPoll = real.pollVerifySession;
|
|
const realRefresh = real.refreshTokenRequest;
|
|
const realCheck = real.checkVerifySession;
|
|
|
|
export interface AuthFlowMockState {
|
|
loginImpl:
|
|
| null
|
|
| ((email: string) => Promise<ApiResult<LoginResponse>> | ApiResult<LoginResponse>);
|
|
pollImpl: null | ((sessionId: string) => Promise<PollResult> | PollResult);
|
|
}
|
|
|
|
export const authFlowMockState: AuthFlowMockState = {
|
|
loginImpl: null,
|
|
pollImpl: null,
|
|
};
|
|
|
|
mock.module("../../src/lib/auth-flow", () => ({
|
|
loginRequest: (email: string, options?: { signal?: AbortSignal }) =>
|
|
authFlowMockState.loginImpl
|
|
? Promise.resolve(authFlowMockState.loginImpl(email))
|
|
: realLogin(email, options),
|
|
pollVerifySession: (
|
|
sessionId: string,
|
|
options?: Parameters<typeof realPoll>[1],
|
|
) =>
|
|
authFlowMockState.pollImpl
|
|
? Promise.resolve(authFlowMockState.pollImpl(sessionId))
|
|
: realPoll(sessionId, options),
|
|
// Always real — used by auth-guard refresh path and by auth-flow.test.ts.
|
|
refreshTokenRequest: (
|
|
refreshToken: string,
|
|
options?: { signal?: AbortSignal },
|
|
): Promise<ApiResult<TokenBundle>> => realRefresh(refreshToken, options),
|
|
checkVerifySession: (
|
|
sessionId: string,
|
|
options?: { signal?: AbortSignal },
|
|
) => realCheck(sessionId, options),
|
|
}));
|
|
|
|
/** Reset both seams to fall through to the real implementation. */
|
|
export function resetAuthFlowMock(): void {
|
|
authFlowMockState.loginImpl = null;
|
|
authFlowMockState.pollImpl = null;
|
|
}
|