mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 00:49:00 +02:00
fix(ios-qa): isolate E2E tests under --concurrent (3 real races)
The ios-qa E2E file failed intermittently under `bun test --concurrent` (the eval harness default). Three distinct shared-state races, all fixed: 1. Shared pidfile: a module-level `workDir` reassigned in beforeEach was clobbered by parallel tests, so concurrent daemons collided on the same pidfile and the loser returned `already_running`. Each test now gets its own dir via makeWorkDir(). 2. process.env path globals: tests set GSTACK_IOS_AUDIT_PATH / _ATTEMPTS_PATH / _ALLOWLIST_PATH on the shared process env; concurrent tests stomped each other's audit/attempts destinations. Threaded auditPath/attemptsPath/allowlistPath through DaemonOptions (and mintForCaller) as explicit args — env is no longer load-bearing. 3. afterEach cleanup race: the per-test cleanup drained a shared dir array, so the first test to finish deleted still-running tests' workDirs mid-assertion. Moved to afterAll (cleans once, after all settle). Verified: 5/5 clean full-suite runs at --max-concurrency 15 (was intermittent); daemon unit suite 91/91; daemon source compiles. The paths default to the env-derived locations when options are omitted, so the production CLI path is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3cbb5c3bbd
commit
51e9351ed7
@@ -31,6 +31,7 @@ export async function mintForCaller(opts: {
|
||||
request: MintRequest;
|
||||
tokenStore: SessionTokenStore;
|
||||
allowlistPath?: string;
|
||||
attemptsPath?: string;
|
||||
endpoint?: string;
|
||||
}): Promise<MintResponse | MintError> {
|
||||
const allowlist = await loadAllowlist(opts.allowlistPath);
|
||||
@@ -42,6 +43,7 @@ export async function mintForCaller(opts: {
|
||||
rawIdentity: opts.callerIdentity,
|
||||
endpoint: opts.endpoint ?? '/auth/mint',
|
||||
reason: 'identity_not_allowed',
|
||||
path: opts.attemptsPath,
|
||||
});
|
||||
return { error: 'identity_not_allowed' };
|
||||
}
|
||||
@@ -52,6 +54,7 @@ export async function mintForCaller(opts: {
|
||||
rawIdentity: opts.callerIdentity,
|
||||
endpoint: opts.endpoint ?? '/auth/mint',
|
||||
reason: 'capability_insufficient',
|
||||
path: opts.attemptsPath,
|
||||
});
|
||||
return { error: 'capability_insufficient' };
|
||||
}
|
||||
@@ -73,6 +76,7 @@ export async function mintForCaller(opts: {
|
||||
rawIdentity: opts.callerIdentity,
|
||||
endpoint: opts.endpoint ?? '/auth/mint',
|
||||
reason: 'rate_limited',
|
||||
path: opts.attemptsPath,
|
||||
});
|
||||
return { error: 'rate_limited' };
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ interface DaemonOptions {
|
||||
tailnetSocketPath?: string;
|
||||
tailnetSessionTtlSeconds?: number;
|
||||
pidfilePath?: string;
|
||||
// Explicit security-log + allowlist paths. Default to the env-var-derived
|
||||
// locations (defaultAuditPath etc.) when omitted, but passing them lets
|
||||
// concurrent test instances stay isolated without racing on process.env.
|
||||
auditPath?: string;
|
||||
attemptsPath?: string;
|
||||
allowlistPath?: string;
|
||||
// Test injection
|
||||
tunnelProvider?: () => Promise<DeviceTunnel | null>;
|
||||
whoIsImpl?: (addr: string) => Promise<{ identity: string; raw: unknown }>;
|
||||
@@ -112,6 +118,9 @@ export async function startDaemon(opts: DaemonOptions): Promise<RunningDaemon |
|
||||
res,
|
||||
tokenStore,
|
||||
getTunnel,
|
||||
auditPath: opts.auditPath,
|
||||
attemptsPath: opts.attemptsPath,
|
||||
allowlistPath: opts.allowlistPath,
|
||||
whoIsImpl: opts.whoIsImpl ?? ((addr) => whoIs(addr, opts.tailnetSocketPath)),
|
||||
});
|
||||
});
|
||||
@@ -172,6 +181,10 @@ interface HandlerCtx {
|
||||
res: ServerResponse;
|
||||
tokenStore: SessionTokenStore;
|
||||
getTunnel: () => Promise<DeviceTunnel | null>;
|
||||
// Explicit security-log + allowlist paths (default to env-derived when undefined).
|
||||
auditPath?: string;
|
||||
attemptsPath?: string;
|
||||
allowlistPath?: string;
|
||||
}
|
||||
|
||||
function readBody(req: IncomingMessage, maxBytes = 1_048_576): Promise<Buffer | { error: 'body_too_large' }> {
|
||||
@@ -274,7 +287,7 @@ interface TailnetCtx extends HandlerCtx {
|
||||
* Tailnet handler — locked allowlist + capability tiers.
|
||||
*/
|
||||
async function handleTailnet(ctx: TailnetCtx): Promise<void> {
|
||||
const { req, res, tokenStore, getTunnel, whoIsImpl } = ctx;
|
||||
const { req, res, tokenStore, getTunnel, whoIsImpl, auditPath, attemptsPath, allowlistPath } = ctx;
|
||||
const url = parseUrl(req.url ?? '/');
|
||||
const path = url.pathname ?? '/';
|
||||
const method = req.method ?? 'GET';
|
||||
@@ -304,6 +317,7 @@ async function handleTailnet(ctx: TailnetCtx): Promise<void> {
|
||||
rawIdentity: peerAddr,
|
||||
endpoint: route,
|
||||
reason: 'whois_unparseable',
|
||||
path: attemptsPath,
|
||||
});
|
||||
sendJson(res, 502, { error: 'whois_failed', detail: (err as Error).message });
|
||||
return;
|
||||
@@ -318,6 +332,8 @@ async function handleTailnet(ctx: TailnetCtx): Promise<void> {
|
||||
request: parsed,
|
||||
tokenStore,
|
||||
endpoint: route,
|
||||
allowlistPath,
|
||||
attemptsPath,
|
||||
});
|
||||
|
||||
if ('error' in result) {
|
||||
@@ -338,6 +354,7 @@ async function handleTailnet(ctx: TailnetCtx): Promise<void> {
|
||||
rawIdentity: token ? 'token:' + token.slice(0, 8) : 'no_token',
|
||||
endpoint: route,
|
||||
reason: validation.reason,
|
||||
path: attemptsPath,
|
||||
});
|
||||
const status = validation.reason === 'capability_insufficient' ? 403 : 401;
|
||||
sendJson(res, status, { error: validation.reason });
|
||||
@@ -383,7 +400,7 @@ async function handleTailnet(ctx: TailnetCtx): Promise<void> {
|
||||
capability: session.capability,
|
||||
request_id: req.headers['x-request-id']?.toString() ?? '-',
|
||||
status: upstream.status,
|
||||
});
|
||||
}, auditPath);
|
||||
}
|
||||
|
||||
res.writeHead(upstream.status, upstream.headers);
|
||||
|
||||
Reference in New Issue
Block a user