Skip to content

Commit e06453e

Browse files
committed
test: fix Windows CI failures — use tmpdir, platform-aware assertions, .mjs plugins
1 parent 219e52e commit e06453e

6 files changed

Lines changed: 53 additions & 34 deletions

File tree

src/agent/multimodal.test.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, it, expect } from 'vitest';
22
import { getTextContent, hasImages, flattenForProvider, loadImage } from './multimodal.js';
33
import type { AgentMessage } from './types.js';
4+
import os from 'node:os';
5+
import path from 'node:path';
46

57
describe('getTextContent', () => {
68
it('returns string content as-is', () => {
@@ -103,25 +105,25 @@ describe('flattenForProvider', () => {
103105
describe('loadImage', () => {
104106
it('loads a PNG file', async () => {
105107
const { writeFile, unlink } = await import('node:fs/promises');
106-
const path = '/tmp/test-jam-multimodal.png';
107-
await writeFile(path, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
108-
const result = await loadImage(path);
108+
const filePath = path.join(os.tmpdir(), 'test-jam-multimodal.png');
109+
await writeFile(filePath, Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
110+
const result = await loadImage(filePath);
109111
expect(result.mediaType).toBe('image/png');
110112
expect(result.data).toBeTruthy();
111113
expect(typeof result.data).toBe('string'); // base64
112-
await unlink(path);
114+
await unlink(filePath);
113115
});
114116

115117
it('detects JPEG media type', async () => {
116118
const { writeFile, unlink } = await import('node:fs/promises');
117-
const path = '/tmp/test-jam-multimodal.jpg';
118-
await writeFile(path, Buffer.from([0xFF, 0xD8, 0xFF]));
119-
const result = await loadImage(path);
119+
const filePath = path.join(os.tmpdir(), 'test-jam-multimodal.jpg');
120+
await writeFile(filePath, Buffer.from([0xFF, 0xD8, 0xFF]));
121+
const result = await loadImage(filePath);
120122
expect(result.mediaType).toBe('image/jpeg');
121-
await unlink(path);
123+
await unlink(filePath);
122124
});
123125

124126
it('throws for non-existent file', async () => {
125-
await expect(loadImage('/tmp/nonexistent-image-12345.png')).rejects.toThrow();
127+
await expect(loadImage(path.join(os.tmpdir(), 'nonexistent-image-12345.png'))).rejects.toThrow();
126128
});
127129
});

src/agent/sandbox.test.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect } from 'vitest';
22
import { detectSandboxStrategy, buildSandboxArgs, executeSandboxed } from './sandbox.js';
33
import type { SandboxConfig } from './types.js';
4+
import os from 'node:os';
45

56
// ── Helpers ───────────────────────────────────────────────────────────────────
67

@@ -54,14 +55,18 @@ describe('buildSandboxArgs', () => {
5455

5556
it('returns shell wrapper for permissions-only', () => {
5657
const result = buildSandboxArgs('echo hello world', workspaceRoot, defaultConfig, 'permissions-only');
57-
expect(result.command).toBe('/bin/sh');
58-
expect(result.args).toEqual(['-c', 'echo hello world']);
58+
const expectedShell = os.platform() === 'win32' ? 'cmd.exe' : '/bin/sh';
59+
const expectedArgs = os.platform() === 'win32' ? ['/c', 'echo hello world'] : ['-c', 'echo hello world'];
60+
expect(result.command).toBe(expectedShell);
61+
expect(result.args).toEqual(expectedArgs);
5962
});
6063

6164
it('returns shell wrapper for permissions-only with no-arg command', () => {
6265
const result = buildSandboxArgs('pwd', workspaceRoot, defaultConfig, 'permissions-only');
63-
expect(result.command).toBe('/bin/sh');
64-
expect(result.args).toEqual(['-c', 'pwd']);
66+
const expectedShell = os.platform() === 'win32' ? 'cmd.exe' : '/bin/sh';
67+
const expectedArgs = os.platform() === 'win32' ? ['/c', 'pwd'] : ['-c', 'pwd'];
68+
expect(result.command).toBe(expectedShell);
69+
expect(result.args).toEqual(expectedArgs);
6570
});
6671

6772
it('includes network deny in sandbox-exec profile when network is blocked', () => {

src/intel/analyzers/typescript.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { TypeScriptAnalyzer } from './typescript.js';
33

44
const analyzer = new TypeScriptAnalyzer();
55

6+
/** Normalize a path to use forward slashes for cross-platform comparison. */
7+
const norm = (p: string) => p.replace(/\\/g, '/');
8+
69
describe('TypeScriptAnalyzer — metadata', () => {
710
it('has correct name and extensions', () => {
811
expect(analyzer.name).toBe('typescript');
@@ -169,15 +172,15 @@ describe('TypeScriptAnalyzer — import edges', () => {
169172
const code = `import { foo } from './foo.js';`;
170173
const { edges } = analyzer.analyzeFile(code, 'src/index.ts', '/root');
171174
const importEdge = edges.find(e => e.type === 'imports');
172-
expect(importEdge!.target).toBe('file:src/foo.ts');
175+
expect(norm(importEdge!.target)).toBe('file:src/foo.ts');
173176
});
174177

175178
it('extracts relative import without extension', () => {
176179
const code = `import { bar } from './utils';`;
177180
const { edges } = analyzer.analyzeFile(code, 'src/index.ts', '/root');
178181
const importEdge = edges.find(e => e.type === 'imports');
179182
expect(importEdge).toBeDefined();
180-
expect(importEdge!.target).toBe('file:src/utils.ts');
183+
expect(norm(importEdge!.target)).toBe('file:src/utils.ts');
181184
});
182185

183186
it('extracts require() as imports edge', () => {
@@ -206,7 +209,7 @@ describe('TypeScriptAnalyzer — import edges', () => {
206209
const code = `import { UserService } from './services/user.js';`;
207210
const { edges } = analyzer.analyzeFile(code, 'src/index.ts', '/root');
208211
const importEdge = edges.find(e => e.type === 'imports');
209-
expect(importEdge!.target).toBe('file:src/services/user.ts');
212+
expect(norm(importEdge!.target)).toBe('file:src/services/user.ts');
210213
});
211214
});
212215

@@ -395,7 +398,7 @@ export default router;
395398
// Import edge (only relative imports)
396399
const importEdges = edges.filter(e => e.type === 'imports');
397400
expect(importEdges).toHaveLength(1);
398-
expect(importEdges[0]!.target).toBe('file:src/routes/services/user.ts');
401+
expect(norm(importEdges[0]!.target)).toBe('file:src/routes/services/user.ts');
399402

400403
// Contains edges
401404
const containsEdges = edges.filter(e => e.type === 'contains');

src/plugins/loader.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ function createPlugin(
1818
mkdirSync(pluginDir, { recursive: true });
1919
writeFileSync(join(pluginDir, 'jam-plugin.json'), JSON.stringify(manifest));
2020
if (indexContent) {
21-
writeFileSync(join(pluginDir, 'index.js'), indexContent);
21+
// Use .mjs so Node.js treats it as ESM regardless of package.json resolution on Windows
22+
writeFileSync(join(pluginDir, 'index.mjs'), indexContent);
2223
}
2324
return pluginDir;
2425
}
2526

2627
describe('discoverPlugins', () => {
2728
it('returns empty array for nonexistent directory', () => {
28-
const result = discoverPlugins(['/tmp/no-such-dir-jam-test']);
29+
const result = discoverPlugins([join(tmpdir(), 'no-such-dir-jam-test')]);
2930
expect(result).toEqual([]);
3031
});
3132

@@ -114,7 +115,8 @@ describe('discoverPlugins', () => {
114115
describe('loadPluginModule', () => {
115116
it('loads a module with register function', async () => {
116117
const dir = createTempDir();
117-
const entryPoint = join(dir, 'index.js');
118+
// Use .mjs so Node.js treats it as ESM regardless of package.json on Windows
119+
const entryPoint = join(dir, 'index.mjs');
118120
writeFileSync(entryPoint, 'export function register() { return "loaded"; }');
119121

120122
const mod = await loadPluginModule(entryPoint);
@@ -123,7 +125,8 @@ describe('loadPluginModule', () => {
123125

124126
it('throws for module without register', async () => {
125127
const dir = createTempDir();
126-
const entryPoint = join(dir, 'index.js');
128+
// Use .mjs so Node.js treats it as ESM regardless of package.json on Windows
129+
const entryPoint = join(dir, 'index.mjs');
127130
writeFileSync(entryPoint, 'export const foo = 42;');
128131

129132
await expect(loadPluginModule(entryPoint)).rejects.toThrow('register');

src/plugins/manager.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ function createPlugin(
1616
writeFileSync(join(pluginDir, 'jam-plugin.json'), JSON.stringify({
1717
name, version, commands: [name],
1818
}));
19-
writeFileSync(join(pluginDir, 'index.js'), code);
19+
// Use .mjs so Node.js treats it as ESM regardless of package.json on Windows
20+
writeFileSync(join(pluginDir, 'index.mjs'), code);
2021
}
2122

2223
describe('PluginManager', () => {
@@ -78,8 +79,8 @@ describe('PluginManager', () => {
7879
writeFileSync(join(pluginDir, 'jam-plugin.json'), JSON.stringify({
7980
name: 'broken', version: '1.0.0', commands: [],
8081
}));
81-
// Broken JS that doesn't export register
82-
writeFileSync(join(pluginDir, 'index.js'), 'export const x = 1;');
82+
// Broken JS that doesn't export register — use .mjs for cross-platform ESM
83+
writeFileSync(join(pluginDir, 'index.mjs'), 'export const x = 1;');
8384

8485
const manager = new PluginManager();
8586
await manager.loadAll([dir]);

src/utils/call-graph.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
3-
import { join } from 'node:path';
3+
import { join, sep } from 'node:path';
44
import { tmpdir } from 'node:os';
55
import {
66
findDefinition,
@@ -12,6 +12,9 @@ import {
1212
formatGraphForAI,
1313
} from './call-graph.js';
1414

15+
/** Normalize a path to use forward slashes for cross-platform comparison. */
16+
const norm = (p: string) => p.replace(/\\/g, '/');
17+
1518
// ── Test workspace setup ─────────────────────────────────────────────────────
1619

1720
let workspace: string;
@@ -92,7 +95,7 @@ describe('findDefinition', () => {
9295
expect(def).not.toBeNull();
9396
expect(def!.name).toBe('processData');
9497
expect(def!.kind).toBe('function');
95-
expect(def!.file).toBe('src/processor.ts');
98+
expect(norm(def!.file)).toBe('src/processor.ts');
9699
expect(def!.params).toContain('input: string');
97100
expect(def!.returnType).toContain('Promise<string>');
98101
});
@@ -113,7 +116,7 @@ describe('findDefinition', () => {
113116
const def = await findDefinition('Logger', workspace);
114117
expect(def).not.toBeNull();
115118
expect(def!.kind).toBe('class');
116-
expect(def!.file).toBe('src/utils.ts');
119+
expect(norm(def!.file)).toBe('src/utils.ts');
117120
});
118121

119122
it('returns null for nonexistent symbol', async () => {
@@ -126,19 +129,21 @@ describe('findDefinition', () => {
126129

127130
describe('findReferences', () => {
128131
it('finds call sites for processData', async () => {
129-
const { callers, imports } = await findReferences('processData', 'src/processor.ts', workspace);
132+
const defFile = ['src', 'processor.ts'].join(sep);
133+
const { callers, imports } = await findReferences('processData', defFile, workspace);
130134

131135
// handler.ts imports and calls processData
132136
expect(imports.length).toBeGreaterThanOrEqual(1);
133-
expect(imports.some((i) => i.file === 'src/handler.ts')).toBe(true);
137+
expect(imports.some((i) => norm(i.file) === 'src/handler.ts')).toBe(true);
134138

135139
expect(callers.length).toBeGreaterThanOrEqual(2);
136-
expect(callers.some((c) => c.file === 'src/handler.ts' && c.args.includes('body'))).toBe(true);
140+
expect(callers.some((c) => norm(c.file) === 'src/handler.ts' && c.args.includes('body'))).toBe(true);
137141
});
138142

139143
it('finds import references for handleRequest', async () => {
140-
const { imports } = await findReferences('handleRequest', 'src/handler.ts', workspace);
141-
expect(imports.some((i) => i.file === 'src/server.ts')).toBe(true);
144+
const defFile = ['src', 'handler.ts'].join(sep);
145+
const { imports } = await findReferences('handleRequest', defFile, workspace);
146+
expect(imports.some((i) => norm(i.file) === 'src/server.ts')).toBe(true);
142147
});
143148
});
144149

@@ -182,7 +187,7 @@ describe('buildCallGraph', () => {
182187
const graph = await buildCallGraph('processData', workspace, { depth: 2 });
183188

184189
expect(graph.symbol.name).toBe('processData');
185-
expect(graph.symbol.file).toBe('src/processor.ts');
190+
expect(norm(graph.symbol.file)).toBe('src/processor.ts');
186191
expect(graph.callers.length).toBeGreaterThanOrEqual(2);
187192
expect(graph.imports.length).toBeGreaterThanOrEqual(1);
188193
expect(graph.callees.length).toBeGreaterThanOrEqual(1);
@@ -204,7 +209,7 @@ describe('formatAsciiTree', () => {
204209

205210
expect(tree).toContain('processData');
206211
expect(tree).toContain('Defined:');
207-
expect(tree).toContain('src/processor.ts');
212+
expect(tree).toContain(['src', 'processor.ts'].join(sep));
208213
});
209214
});
210215

0 commit comments

Comments
 (0)