Skip to content

Commit 0ef8314

Browse files
Stephen Millerclaude
authored andcommitted
feat: add vitest test runner with fixture loader and mock activation
Configures vitest as the primary test runner for the v6 Local Testing & Simulation Framework (Phase 47: Mock Infrastructure, issue #249). Changes: - package.json: add vitest ^2.0.0 devDependency; npm test → vitest run; npm run test:watch → vitest; npm run test:node preserves existing node --test runner for .test.cjs files - vitest.config.js: node environment, setupFiles: ['./test/setup.js'], includes .test.js/.spec.js, excludes .test.cjs (node:test incompatible) - test/setup.js: auto-activates mock-github and mock-gsd-agent in beforeEach/afterEach; conditional require — works when PRs #258/#259 are not yet merged to main; exports mockGitHub/mockGsdAgent - test/loadFixture.js: loadFixture(name) helper that reads from test/fixtures/; supports namespaced paths (e.g. 'github/issue-view'); throws descriptive errors on missing/invalid fixtures Closes #249 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent e352921 commit 0ef8314

4 files changed

Lines changed: 170 additions & 2 deletions

File tree

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"scripts": {
1717
"build": "pkgroll --clean-dist --src .",
1818
"dev": "pkgroll --watch --src .",
19-
"test": "node --test test/*.test.cjs",
19+
"test": "vitest run",
20+
"test:watch": "vitest",
21+
"test:node": "node --test test/*.test.cjs",
2022
"lint": "eslint lib/ bin/ test/",
2123
"prepublishOnly": "npm run build",
2224
"completions": "node bin/generate-completions.cjs",
@@ -31,7 +33,8 @@
3133
"devDependencies": {
3234
"@eslint/js": "^10.0.1",
3335
"eslint": "^10.0.2",
34-
"pkgroll": "^2.26.3"
36+
"pkgroll": "^2.26.3",
37+
"vitest": "^2.0.0"
3538
},
3639
"engines": {
3740
"node": ">=18.0.0"

test/loadFixture.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* test/loadFixture.js — Fixture loader helper
3+
*
4+
* Loads JSON fixture files from test/fixtures/.
5+
*
6+
* Usage:
7+
*
8+
* import { loadFixture } from './loadFixture.js';
9+
*
10+
* const issueData = loadFixture('github/issue-view');
11+
* const plannerOutput = loadFixture('agents/gsd-planner');
12+
*
13+
* Fixture file resolution:
14+
* loadFixture('github/issue-view') → test/fixtures/github/issue-view.json
15+
* loadFixture('agents/gsd-planner') → test/fixtures/agents/gsd-planner.json
16+
* loadFixture('my-fixture') → test/fixtures/my-fixture.json
17+
*
18+
* @param {string} name - Fixture name, optionally prefixed with subdirectory.
19+
* Forward slashes are used as path separators (platform-independent).
20+
* @returns {unknown} Parsed JSON content of the fixture file.
21+
* @throws {Error} if the fixture file is not found.
22+
* @throws {Error} if the fixture file contains invalid JSON.
23+
*/
24+
25+
import { fileURLToPath } from 'url';
26+
import path from 'path';
27+
import fs from 'fs';
28+
29+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
30+
31+
/**
32+
* Absolute path to the test/fixtures/ directory.
33+
* All fixture names are resolved relative to this directory.
34+
*/
35+
export const FIXTURES_DIR = path.resolve(__dirname, 'fixtures');
36+
37+
/**
38+
* Load a fixture by name and return its parsed JSON content.
39+
*
40+
* @param {string} name - Fixture identifier, e.g. 'github/issue-view' or 'agents/gsd-planner'
41+
* @returns {unknown} Parsed JSON value — may be an object, array, string, number, or boolean.
42+
*/
43+
export function loadFixture(name) {
44+
// Normalize forward slashes to platform path separator
45+
const normalizedName = name.split('/').join(path.sep);
46+
const fixturePath = path.resolve(FIXTURES_DIR, `${normalizedName}.json`);
47+
48+
if (!fs.existsSync(fixturePath)) {
49+
throw new Error(
50+
`loadFixture: fixture not found: "${name}"\n` +
51+
` Looked for: ${fixturePath}\n` +
52+
` Fixtures directory: ${FIXTURES_DIR}`
53+
);
54+
}
55+
56+
const raw = fs.readFileSync(fixturePath, 'utf-8').trim();
57+
58+
try {
59+
return JSON.parse(raw);
60+
} catch (err) {
61+
throw new Error(
62+
`loadFixture: fixture "${name}" is not valid JSON (${fixturePath}): ${err.message}`
63+
);
64+
}
65+
}

test/setup.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* test/setup.js — Vitest global setup
3+
*
4+
* Auto-activates mock-github and mock-gsd-agent before each test, and
5+
* deactivates them after. Both mocks are conditionally required — the
6+
* setup works correctly even when lib/mock-github.cjs or
7+
* lib/mock-gsd-agent.cjs are not yet present (e.g., when PRs #258 and
8+
* #259 are not yet merged to main).
9+
*
10+
* To use mocks in a vitest test file:
11+
*
12+
* import { mockGitHub, mockGsdAgent } from './setup.js';
13+
*
14+
* test('my test', () => {
15+
* // mocks are already active (activated in beforeEach)
16+
* mockGitHub.setResponse('gh issue view', '{"number":999}');
17+
* // ...
18+
* });
19+
*
20+
* To use a scenario:
21+
*
22+
* import { mockGitHub } from './setup.js';
23+
*
24+
* beforeEach(() => {
25+
* // Override the global beforeEach activation with a scenario
26+
* mockGitHub.deactivate();
27+
* mockGitHub.activate('pr-error');
28+
* });
29+
*/
30+
31+
import { beforeEach, afterEach } from 'vitest';
32+
import { createRequire } from 'module';
33+
import { fileURLToPath } from 'url';
34+
import path from 'path';
35+
36+
const require = createRequire(import.meta.url);
37+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
38+
const repoRoot = path.resolve(__dirname, '..');
39+
40+
// ---------------------------------------------------------------------------
41+
// Conditional mock loading
42+
// ---------------------------------------------------------------------------
43+
44+
// Conditionally load mock-github — gracefully skip if not present
45+
// (lib/mock-github.cjs lands via PR #258)
46+
let mockGitHub = null;
47+
try {
48+
mockGitHub = require(path.join(repoRoot, 'lib', 'mock-github.cjs'));
49+
} catch (_e) {
50+
// mock-github.cjs not available — tests run without GitHub API interception
51+
}
52+
53+
// Conditionally load mock-gsd-agent — gracefully skip if not present
54+
// (lib/mock-gsd-agent.cjs lands via PR #259)
55+
let mockGsdAgent = null;
56+
try {
57+
mockGsdAgent = require(path.join(repoRoot, 'lib', 'mock-gsd-agent.cjs'));
58+
} catch (_e) {
59+
// mock-gsd-agent.cjs not available — tests run without agent spawn interception
60+
}
61+
62+
// ---------------------------------------------------------------------------
63+
// Auto-activate hooks
64+
// ---------------------------------------------------------------------------
65+
66+
beforeEach(() => {
67+
if (mockGitHub && typeof mockGitHub.activate === 'function') {
68+
mockGitHub.activate();
69+
}
70+
if (mockGsdAgent && typeof mockGsdAgent.activate === 'function') {
71+
mockGsdAgent.activate();
72+
}
73+
});
74+
75+
afterEach(() => {
76+
if (mockGitHub && typeof mockGitHub.deactivate === 'function') {
77+
mockGitHub.deactivate();
78+
}
79+
if (mockGsdAgent && typeof mockGsdAgent.deactivate === 'function') {
80+
mockGsdAgent.deactivate();
81+
}
82+
});
83+
84+
// ---------------------------------------------------------------------------
85+
// Exports — available for test files that need direct mock access
86+
// ---------------------------------------------------------------------------
87+
88+
export { mockGitHub, mockGsdAgent };

vitest.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
environment: 'node',
6+
setupFiles: ['./test/setup.js'],
7+
// Target .test.js and .spec.js files for vitest
8+
// Exclude .test.cjs files — those use node:test (run via npm run test:node)
9+
include: ['test/**/*.{test,spec}.{js,mjs}'],
10+
exclude: ['test/**/*.test.cjs'],
11+
},
12+
});

0 commit comments

Comments
 (0)