Skip to content

Commit 203512e

Browse files
author
StackMemory Bot (CLI)
committed
fix(test): replace flaky execSync team tests with programmatic invocation
team.test.ts was timing out (ETIMEDOUT) spawning `npx tsx` for 8 tests. Replaced with direct commander `parseAsync()` invocation — runs in 166ms instead of 135s+. Also adds 15 new tests for config types and update-checker.
1 parent f0cd1a7 commit 203512e

3 files changed

Lines changed: 237 additions & 70 deletions

File tree

src/cli/commands/__tests__/team.test.ts

Lines changed: 85 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
22
import Database from 'better-sqlite3';
3-
import { mkdtempSync, mkdirSync, copyFileSync } from 'fs';
3+
import { mkdtempSync, mkdirSync, copyFileSync, existsSync } from 'fs';
44
import { join } from 'path';
55
import { tmpdir } from 'os';
66
import { execSync } from 'child_process';
7+
import { createTeamCommands } from '../team.js';
78

89
/**
910
* Create a minimal .stackmemory/context.db that FrameManager can open.
@@ -66,33 +67,70 @@ function setupProjectWithData(dir: string): Database.Database {
6667
return db;
6768
}
6869

69-
const cliPath = join(__dirname, '..', '..', 'index.ts');
70+
/**
71+
* Run a team command programmatically via commander.
72+
* Temporarily changes cwd and captures console output.
73+
*/
74+
async function runTeamCommand(
75+
args: string[],
76+
cwd: string
77+
): Promise<{ stdout: string; exitCode: number }> {
78+
const originalCwd = process.cwd();
79+
const originalExitCode = process.exitCode;
80+
process.chdir(cwd);
81+
process.exitCode = 0;
82+
83+
const logs: string[] = [];
84+
const spy = vi.spyOn(console, 'log').mockImplementation((...a) => {
85+
logs.push(a.map(String).join(' '));
86+
});
87+
const errSpy = vi.spyOn(console, 'error').mockImplementation((...a) => {
88+
logs.push(a.map(String).join(' '));
89+
});
90+
91+
try {
92+
const cmd = createTeamCommands();
93+
// Commander expects program name + subcommand in argv
94+
await cmd.parseAsync(['node', 'team', ...args]);
95+
} finally {
96+
spy.mockRestore();
97+
errSpy.mockRestore();
98+
process.chdir(originalCwd);
99+
}
100+
101+
const exitCode = process.exitCode ?? 0;
102+
process.exitCode = originalExitCode;
103+
return { stdout: logs.join('\n'), exitCode };
104+
}
70105

71106
describe('team CLI commands', () => {
72107
let tmpDir: string;
73-
let originalCwd: string;
74108

75109
beforeEach(() => {
76110
tmpDir = mkdtempSync(join(tmpdir(), 'sm-team-test-'));
77-
originalCwd = process.cwd();
78-
});
79-
80-
afterEach(() => {
81-
process.chdir(originalCwd);
82111
});
83112

84113
describe('team share', () => {
85-
it('should create shared anchor with correct metadata', () => {
114+
it('should create shared anchor with correct metadata', async () => {
86115
setupEmptyProject(tmpDir);
87-
process.chdir(tmpDir);
88116

89-
const result = execSync(
90-
`npx tsx ${cliPath} team share -c "API endpoint is /v2/users" -t DECISION -p 9 --source manual`,
91-
{ cwd: tmpDir, encoding: 'utf-8', timeout: 15000 }
117+
const { stdout } = await runTeamCommand(
118+
[
119+
'share',
120+
'-c',
121+
'API endpoint is /v2/users',
122+
'-t',
123+
'DECISION',
124+
'-p',
125+
'9',
126+
'--source',
127+
'manual',
128+
],
129+
tmpDir
92130
);
93131

94-
expect(result).toContain('[DECISION]');
95-
expect(result).toContain('priority 9');
132+
expect(stdout).toContain('[DECISION]');
133+
expect(stdout).toContain('priority 9');
96134

97135
const checkDb = new Database(join(tmpDir, '.stackmemory', 'context.db'));
98136
const anchors = checkDb
@@ -116,15 +154,10 @@ describe('team CLI commands', () => {
116154
checkDb.close();
117155
});
118156

119-
it('should default to type=FACT priority=7', () => {
157+
it('should default to type=FACT priority=7', async () => {
120158
setupEmptyProject(tmpDir);
121-
process.chdir(tmpDir);
122159

123-
execSync(`npx tsx ${cliPath} team share -c "some fact"`, {
124-
cwd: tmpDir,
125-
encoding: 'utf-8',
126-
timeout: 15000,
127-
});
160+
await runTeamCommand(['share', '-c', 'some fact'], tmpDir);
128161

129162
const checkDb = new Database(join(tmpDir, '.stackmemory', 'context.db'));
130163
const anchors = checkDb
@@ -137,15 +170,10 @@ describe('team CLI commands', () => {
137170
checkDb.close();
138171
});
139172

140-
it('should auto-create frame if none active', () => {
173+
it('should auto-create frame if none active', async () => {
141174
setupEmptyProject(tmpDir);
142-
process.chdir(tmpDir);
143175

144-
execSync(`npx tsx ${cliPath} team share -c "auto-frame test"`, {
145-
cwd: tmpDir,
146-
encoding: 'utf-8',
147-
timeout: 15000,
148-
});
176+
await runTeamCommand(['share', '-c', 'auto-frame test'], tmpDir);
149177

150178
const checkDb = new Database(join(tmpDir, '.stackmemory', 'context.db'));
151179
const frames = checkDb.prepare(`SELECT * FROM frames`).all() as Array<{
@@ -157,13 +185,22 @@ describe('team CLI commands', () => {
157185
checkDb.close();
158186
});
159187

160-
it('should store source, agentId, taskId in metadata', () => {
188+
it('should store source, agentId, taskId in metadata', async () => {
161189
setupEmptyProject(tmpDir);
162-
process.chdir(tmpDir);
163190

164-
execSync(
165-
`npx tsx ${cliPath} team share -c "context with ids" --source subagent --agent-id agent-1 --task-id task-42`,
166-
{ cwd: tmpDir, encoding: 'utf-8', timeout: 15000 }
191+
await runTeamCommand(
192+
[
193+
'share',
194+
'-c',
195+
'context with ids',
196+
'--source',
197+
'subagent',
198+
'--agent-id',
199+
'agent-1',
200+
'--task-id',
201+
'task-42',
202+
],
203+
tmpDir
167204
);
168205

169206
const checkDb = new Database(join(tmpDir, '.stackmemory', 'context.db'));
@@ -180,16 +217,11 @@ describe('team CLI commands', () => {
180217
checkDb.close();
181218
});
182219

183-
it('should truncate content > 2000 chars', () => {
220+
it('should truncate content > 2000 chars', async () => {
184221
setupEmptyProject(tmpDir);
185-
process.chdir(tmpDir);
186222

187223
const longContent = 'x'.repeat(3000);
188-
execSync(`npx tsx ${cliPath} team share -c "${longContent}"`, {
189-
cwd: tmpDir,
190-
encoding: 'utf-8',
191-
timeout: 15000,
192-
});
224+
await runTeamCommand(['share', '-c', longContent], tmpDir);
193225

194226
const checkDb = new Database(join(tmpDir, '.stackmemory', 'context.db'));
195227
const anchors = checkDb
@@ -204,7 +236,7 @@ describe('team CLI commands', () => {
204236
});
205237

206238
describe('team list', () => {
207-
it('should list shared anchors', () => {
239+
it('should list shared anchors', async () => {
208240
const db = setupProjectWithData(tmpDir);
209241
const now = Math.floor(Date.now() / 1000);
210242
db.prepare(
@@ -217,20 +249,14 @@ describe('team CLI commands', () => {
217249
).run(now - 30);
218250
db.close();
219251

220-
process.chdir(tmpDir);
221-
222-
const result = execSync(`npx tsx ${cliPath} team list`, {
223-
cwd: tmpDir,
224-
encoding: 'utf-8',
225-
timeout: 15000,
226-
});
252+
const { stdout } = await runTeamCommand(['list'], tmpDir);
227253

228-
expect(result).toContain('shared finding');
229-
expect(result).toContain('[FACT]');
230-
expect(result).toContain('p8');
254+
expect(stdout).toContain('shared finding');
255+
expect(stdout).toContain('[FACT]');
256+
expect(stdout).toContain('p8');
231257
});
232258

233-
it('should respect --limit', () => {
259+
it('should respect --limit', async () => {
234260
const db = setupProjectWithData(tmpDir);
235261
const now = Math.floor(Date.now() / 1000);
236262

@@ -247,29 +273,18 @@ describe('team CLI commands', () => {
247273
}
248274
db.close();
249275

250-
process.chdir(tmpDir);
276+
const { stdout } = await runTeamCommand(['list', '--limit', '2'], tmpDir);
251277

252-
const result = execSync(`npx tsx ${cliPath} team list --limit 2`, {
253-
cwd: tmpDir,
254-
encoding: 'utf-8',
255-
timeout: 15000,
256-
});
257-
258-
expect(result).toContain('2 anchors');
278+
expect(stdout).toContain('2 anchors');
259279
});
260280

261-
it('should show no results when no shared anchors exist', () => {
281+
it('should show no results when no shared anchors exist', async () => {
262282
const db = setupProjectWithData(tmpDir);
263283
db.close();
264-
process.chdir(tmpDir);
265284

266-
const result = execSync(`npx tsx ${cliPath} team list`, {
267-
cwd: tmpDir,
268-
encoding: 'utf-8',
269-
timeout: 15000,
270-
});
285+
const { stdout } = await runTeamCommand(['list'], tmpDir);
271286

272-
expect(result).toContain('No shared context found');
287+
expect(stdout).toContain('No shared context found');
273288
});
274289
});
275290

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
DEFAULT_WEIGHTS,
4+
DEFAULT_TOOL_SCORES,
5+
DEFAULT_CONFIG,
6+
PRESET_PROFILES,
7+
} from '../types.js';
8+
9+
describe('config types', () => {
10+
describe('DEFAULT_WEIGHTS', () => {
11+
it('sums to 1.0', () => {
12+
const sum =
13+
DEFAULT_WEIGHTS.base +
14+
DEFAULT_WEIGHTS.impact +
15+
DEFAULT_WEIGHTS.persistence +
16+
DEFAULT_WEIGHTS.reference;
17+
expect(sum).toBeCloseTo(1.0);
18+
});
19+
20+
it('has all required fields', () => {
21+
expect(DEFAULT_WEIGHTS.base).toBeGreaterThan(0);
22+
expect(DEFAULT_WEIGHTS.impact).toBeGreaterThan(0);
23+
expect(DEFAULT_WEIGHTS.persistence).toBeGreaterThan(0);
24+
expect(DEFAULT_WEIGHTS.reference).toBeGreaterThan(0);
25+
});
26+
});
27+
28+
describe('DEFAULT_TOOL_SCORES', () => {
29+
it('has scores between 0 and 1', () => {
30+
for (const [, score] of Object.entries(DEFAULT_TOOL_SCORES)) {
31+
if (score !== undefined) {
32+
expect(score).toBeGreaterThanOrEqual(0);
33+
expect(score).toBeLessThanOrEqual(1);
34+
}
35+
}
36+
});
37+
38+
it('ranks search highest', () => {
39+
expect(DEFAULT_TOOL_SCORES.search).toBeGreaterThan(
40+
DEFAULT_TOOL_SCORES.read!
41+
);
42+
expect(DEFAULT_TOOL_SCORES.search).toBeGreaterThan(
43+
DEFAULT_TOOL_SCORES.bash!
44+
);
45+
});
46+
});
47+
48+
describe('PRESET_PROFILES', () => {
49+
it('includes default, security-focused, exploration-heavy, production-system', () => {
50+
expect(PRESET_PROFILES).toHaveProperty('default');
51+
expect(PRESET_PROFILES).toHaveProperty('security-focused');
52+
expect(PRESET_PROFILES).toHaveProperty('exploration-heavy');
53+
expect(PRESET_PROFILES).toHaveProperty('production-system');
54+
});
55+
56+
it('all profiles have name matching key', () => {
57+
for (const [key, profile] of Object.entries(PRESET_PROFILES)) {
58+
expect(profile.name).toBe(key);
59+
}
60+
});
61+
62+
it('security profile prioritizes impact weight', () => {
63+
const secProfile = PRESET_PROFILES['security-focused'];
64+
expect(secProfile.scoring?.weights?.impact).toBeGreaterThan(
65+
secProfile.scoring?.weights?.base!
66+
);
67+
});
68+
});
69+
70+
describe('DEFAULT_CONFIG', () => {
71+
it('has version field', () => {
72+
expect(DEFAULT_CONFIG.version).toBeDefined();
73+
});
74+
75+
it('has scoring with weights and tool_scores', () => {
76+
expect(DEFAULT_CONFIG.scoring.weights).toEqual(DEFAULT_WEIGHTS);
77+
expect(DEFAULT_CONFIG.scoring.tool_scores).toEqual(DEFAULT_TOOL_SCORES);
78+
});
79+
80+
it('has retention config', () => {
81+
expect(DEFAULT_CONFIG.retention.local.young).toBe('1d');
82+
expect(DEFAULT_CONFIG.retention.remote.enabled).toBe(true);
83+
});
84+
85+
it('has performance config', () => {
86+
expect(DEFAULT_CONFIG.performance.max_stack_depth).toBeGreaterThan(0);
87+
expect(DEFAULT_CONFIG.performance.retrieval_timeout_ms).toBeGreaterThan(
88+
0
89+
);
90+
});
91+
92+
it('includes preset profiles', () => {
93+
expect(DEFAULT_CONFIG.profiles).toBe(PRESET_PROFILES);
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)