Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions test/credentials-cross-process.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { spawn } from 'node:child_process';
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { beforeAll, describe, expect, it } from 'vitest';
import { readCredentialsFile } from '../src/lib/credentials.js';
import { execNpm } from './helpers/execNpm.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, '..');
const CHILD_PATH = join(REPO_ROOT, 'test', 'helpers', 'credentials-write-child.mjs');

interface ChildResult {
code: number | null;
signal: NodeJS.Signals | null;
stderr: string;
stdout: string;
}

function runCredentialWriter(env: Record<string, string>): Promise<ChildResult> {
return new Promise((resolveChild, reject) => {
const child = spawn(process.execPath, [CHILD_PATH], {
cwd: REPO_ROOT,
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe'],
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const timer = setTimeout(() => {
child.kill();
}, 10_000);

child.stdout.on('data', chunk => stdout.push(Buffer.from(chunk)));
child.stderr.on('data', chunk => stderr.push(Buffer.from(chunk)));
child.on('error', error => {
clearTimeout(timer);
reject(error);
});
child.on('close', (code, signal) => {
clearTimeout(timer);
resolveChild({
code,
signal,
stdout: Buffer.concat(stdout).toString('utf8'),
stderr: Buffer.concat(stderr).toString('utf8'),
});
});
});
}

describe('credentials cross-process writes', () => {
beforeAll(() => {
execNpm(['run', 'build'], { cwd: REPO_ROOT, stdio: 'pipe' });
});
Comment on lines +52 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
sed -n '1,90p' test/credentials-cross-process.test.ts | cat -n

echo
echo "== package/test scripts and deps =="
if [ -f package.json ]; then
  jq '{scripts, devDependencies, dependencies, packageManager}' package.json
fi

echo
echo "== relevant lock/vitest config if present =="
for f in vitest.config.* vite.config.* package-lock.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    if [ "$f" = package-lock.json ]; then
      jq -r '.lockfileVersion, .packages[""].devDependencies.vitest // empty, .packages["node_modules/vitest"].version // empty' "$f"
    else
      rg -n "vitest|hookTimeout|testTimeout|timeout" "$f" | head -40 || true
    fi
  fi
done

echo
echo "== all vitest hookTimeout / beforeAll usages =="
rg -n "hookTimeout|beforeAll\\(" test package.json vitest.config.* vite.config.* 2>/dev/null || true

echo
echo "== repo build script excerpt =="
if [ -f package.json ]; then
  jq -e '.scripts.build // empty' package.json
fi

Repository: TestSprite/testsprite-cli

Length of output: 5634


🌐 Web query:

vitest v2 default hookTimeout beforeAll timeout documentation

💡 Result:

In Vitest, including v2, the default timeout for hooks like beforeAll is 10,000 milliseconds (10 seconds) when running in a Node.js environment [1][2][3]. If browser mode is enabled, the default timeout increases to 30,000 milliseconds (30 seconds) [1][3][4]. You can override this global setting using the hookTimeout configuration option [1][2]: // vitest.config.ts export default { test: { hookTimeout: 20000, // Sets hook timeout to 20 seconds }, } Additionally, you can specify a custom timeout for an individual beforeAll hook by passing the timeout as the second argument [2][5]: beforeAll(async => { // your setup logic }, 20000); // Individual timeout of 20 seconds Setting the timeout to 0 disables it completely [1][3][6].

Citations:


Set an explicit timeout for the beforeAll build hook.

This beforeAll runs npm run build via tsc and a postbuild script, but the hook uses Vitest’s default 10s timeout. That can fail the test run with a generic hook-timeout error instead of a clear build failure.

🔧 Suggested fix
-  beforeAll(() => {
+  beforeAll(() => {
     execNpm(['run', 'build'], { cwd: REPO_ROOT, stdio: 'pipe' });
-  });
+  }, 60_000);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('credentials cross-process writes', () => {
beforeAll(() => {
execNpm(['run', 'build'], { cwd: REPO_ROOT, stdio: 'pipe' });
});
describe('credentials cross-process writes', () => {
beforeAll(() => {
execNpm(['run', 'build'], { cwd: REPO_ROOT, stdio: 'pipe' });
}, 60_000);
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/credentials-cross-process.test.ts` around lines 52 - 55, Set an explicit
timeout on the beforeAll build hook in the “credentials cross-process writes”
suite, long enough for execNpm(['run', 'build'], ...) to complete reliably. Keep
the existing build command and test behavior unchanged.


it('preserves every profile written by concurrent child processes', async () => {
const tmpRoot = mkdtempSync(join(tmpdir(), 'testsprite-creds-race-'));
const credentialsPath = join(tmpRoot, 'credentials');
const startPath = join(tmpRoot, 'start');

try {
const profiles = Array.from({ length: 8 }, (_, index) => ({
apiKey: `sk-child-${index}`,
profile: `child-${index}`,
}));
const children = profiles.map(({ apiKey, profile }) =>
runCredentialWriter({
CRED_API_KEY: apiKey,
CRED_PATH: credentialsPath,
CRED_PROFILE: profile,
CRED_START_PATH: startPath,
}),
);

await new Promise(resolveDelay => setTimeout(resolveDelay, 50));
writeFileSync(startPath, 'go');

const results = await Promise.all(children);
expect(results).toEqual(
profiles.map(() => ({
code: 0,
signal: null,
stderr: '',
stdout: '',
})),
);

const credentials = readCredentialsFile({ path: credentialsPath });
for (const { apiKey, profile } of profiles) {
expect(credentials[profile]).toEqual({ apiKey });
}
expect(existsSync(`${credentialsPath}.lock`)).toBe(false);
Comment on lines +57 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Only the success path is covered; the helper's error/exit-code paths are untested.

The child helper documents three distinct failure exit codes (1: missing env vars, 2: start-marker timeout, 3: writeProfile error), but this suite only ever exercises the code: 0 success path across all 8 children. Per path instructions, new behavior — including error and exit-code paths — should be covered.

As per path instructions, "Cover new behavior, including error and exit-code paths." Consider adding focused cases, e.g. omitting a required env var (expect code 1) and pointing CRED_START_PATH at a marker that's never created with a shortened wait (expect code 2).

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from 'node:child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/credentials-cross-process.test.ts` around lines 57 - 93, Add focused
tests for the failure branches of runCredentialWriter alongside the existing
success test: omit a required credential environment variable and assert exit
code 1, and configure an uncreated CRED_START_PATH with a shortened timeout and
assert exit code 2. Reuse the existing temporary-directory setup and
child-result assertions, and cover the writeProfile error path with exit code 3
if the helper exposes a controllable failure trigger.

Source: Path instructions

} finally {
rmSync(tmpRoot, { force: true, recursive: true });
}
}, 20_000);
});
28 changes: 28 additions & 0 deletions test/helpers/credentials-write-child.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { existsSync } from 'node:fs';
import { writeProfile } from '../../dist/lib/credentials.js';

const profile = process.env.CRED_PROFILE;
const credentialsPath = process.env.CRED_PATH;
const apiKey = process.env.CRED_API_KEY;
const startPath = process.env.CRED_START_PATH;

if (!profile || !credentialsPath || !apiKey || !startPath) {
console.error('CRED_PROFILE, CRED_PATH, CRED_API_KEY, and CRED_START_PATH are required');
process.exit(1);
}

const deadline = Date.now() + 5_000;
while (!existsSync(startPath)) {
if (Date.now() >= deadline) {
console.error(`Timed out waiting for start marker: ${startPath}`);
process.exit(2);
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
}

try {
writeProfile(profile, { apiKey }, { path: credentialsPath });
} catch (error) {
console.error(error instanceof Error ? error.stack : String(error));
process.exit(3);
}
Loading