Skip to content

Commit ff5ee23

Browse files
test: cover cross-process credential writes
1 parent fe07bc9 commit ff5ee23

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { spawn } from 'node:child_process';
2+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3+
import { tmpdir } from 'node:os';
4+
import { dirname, join, resolve } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
import { beforeAll, describe, expect, it } from 'vitest';
7+
import { readCredentialsFile } from '../src/lib/credentials.js';
8+
import { execNpm } from './helpers/execNpm.js';
9+
10+
const __dirname = dirname(fileURLToPath(import.meta.url));
11+
const REPO_ROOT = resolve(__dirname, '..');
12+
const CHILD_PATH = join(REPO_ROOT, 'test', 'helpers', 'credentials-write-child.mjs');
13+
14+
interface ChildResult {
15+
code: number | null;
16+
signal: NodeJS.Signals | null;
17+
stderr: string;
18+
stdout: string;
19+
}
20+
21+
function runCredentialWriter(env: Record<string, string>): Promise<ChildResult> {
22+
return new Promise((resolveChild, reject) => {
23+
const child = spawn(process.execPath, [CHILD_PATH], {
24+
cwd: REPO_ROOT,
25+
env: { ...process.env, ...env },
26+
stdio: ['ignore', 'pipe', 'pipe'],
27+
});
28+
const stdout: Buffer[] = [];
29+
const stderr: Buffer[] = [];
30+
const timer = setTimeout(() => {
31+
child.kill();
32+
}, 10_000);
33+
34+
child.stdout.on('data', chunk => stdout.push(Buffer.from(chunk)));
35+
child.stderr.on('data', chunk => stderr.push(Buffer.from(chunk)));
36+
child.on('error', error => {
37+
clearTimeout(timer);
38+
reject(error);
39+
});
40+
child.on('close', (code, signal) => {
41+
clearTimeout(timer);
42+
resolveChild({
43+
code,
44+
signal,
45+
stdout: Buffer.concat(stdout).toString('utf8'),
46+
stderr: Buffer.concat(stderr).toString('utf8'),
47+
});
48+
});
49+
});
50+
}
51+
52+
describe('credentials cross-process writes', () => {
53+
beforeAll(() => {
54+
execNpm(['run', 'build'], { cwd: REPO_ROOT, stdio: 'pipe' });
55+
});
56+
57+
it('preserves every profile written by concurrent child processes', async () => {
58+
const tmpRoot = mkdtempSync(join(tmpdir(), 'testsprite-creds-race-'));
59+
const credentialsPath = join(tmpRoot, 'credentials');
60+
const startPath = join(tmpRoot, 'start');
61+
62+
try {
63+
const profiles = Array.from({ length: 8 }, (_, index) => ({
64+
apiKey: `sk-child-${index}`,
65+
profile: `child-${index}`,
66+
}));
67+
const children = profiles.map(({ apiKey, profile }) =>
68+
runCredentialWriter({
69+
CRED_API_KEY: apiKey,
70+
CRED_PATH: credentialsPath,
71+
CRED_PROFILE: profile,
72+
CRED_START_PATH: startPath,
73+
}),
74+
);
75+
76+
await new Promise(resolveDelay => setTimeout(resolveDelay, 50));
77+
writeFileSync(startPath, 'go');
78+
79+
const results = await Promise.all(children);
80+
expect(results).toEqual(
81+
profiles.map(() => ({
82+
code: 0,
83+
signal: null,
84+
stderr: '',
85+
stdout: '',
86+
})),
87+
);
88+
89+
const credentials = readCredentialsFile({ path: credentialsPath });
90+
for (const { apiKey, profile } of profiles) {
91+
expect(credentials[profile]).toEqual({ apiKey });
92+
}
93+
expect(existsSync(`${credentialsPath}.lock`)).toBe(false);
94+
} finally {
95+
rmSync(tmpRoot, { force: true, recursive: true });
96+
}
97+
}, 20_000);
98+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { existsSync } from 'node:fs';
2+
import { writeProfile } from '../../dist/lib/credentials.js';
3+
4+
const profile = process.env.CRED_PROFILE;
5+
const credentialsPath = process.env.CRED_PATH;
6+
const apiKey = process.env.CRED_API_KEY;
7+
const startPath = process.env.CRED_START_PATH;
8+
9+
if (!profile || !credentialsPath || !apiKey || !startPath) {
10+
console.error('CRED_PROFILE, CRED_PATH, CRED_API_KEY, and CRED_START_PATH are required');
11+
process.exit(1);
12+
}
13+
14+
const deadline = Date.now() + 5_000;
15+
while (!existsSync(startPath)) {
16+
if (Date.now() >= deadline) {
17+
console.error(`Timed out waiting for start marker: ${startPath}`);
18+
process.exit(2);
19+
}
20+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
21+
}
22+
23+
try {
24+
writeProfile(profile, { apiKey }, { path: credentialsPath });
25+
} catch (error) {
26+
console.error(error instanceof Error ? error.stack : String(error));
27+
process.exit(3);
28+
}

0 commit comments

Comments
 (0)