Skip to content

Commit 8b80eb1

Browse files
author
StackMemory Bot (CLI)
committed
test(digest): add unit + CLI tests, wire tool count into doctor
- 21 unit tests for chronological-digest (periods, fallback, rendering, stats) - 6 CLI tests for digest command (today/yesterday/week, custom output) - Doctor now checks MCP tool definitions count and verifies key tools (sm_digest, cord_spawn, team_search, get_context, create_task)
1 parent a1a4cc1 commit 8b80eb1

3 files changed

Lines changed: 569 additions & 2 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { Command } from 'commander';
3+
import Database from 'better-sqlite3';
4+
import { mkdtempSync, existsSync, readFileSync, mkdirSync } from 'fs';
5+
import { join } from 'path';
6+
import { tmpdir } from 'os';
7+
import { createDigestCommands } from '../digest.js';
8+
import { execSync } from 'child_process';
9+
10+
function setupTestProject(dir: string): void {
11+
// Create .git so findProjectRoot() stops here
12+
mkdirSync(join(dir, '.git'));
13+
// Create .stackmemory with a seeded database
14+
const smDir = join(dir, '.stackmemory');
15+
mkdirSync(smDir);
16+
17+
const db = new Database(join(smDir, 'context.db'));
18+
db.exec(`
19+
CREATE TABLE frames (
20+
frame_id TEXT PRIMARY KEY,
21+
run_id TEXT NOT NULL,
22+
project_id TEXT NOT NULL,
23+
parent_frame_id TEXT,
24+
depth INTEGER NOT NULL DEFAULT 0,
25+
type TEXT NOT NULL,
26+
name TEXT NOT NULL,
27+
state TEXT DEFAULT 'active',
28+
inputs TEXT DEFAULT '{}',
29+
outputs TEXT DEFAULT '{}',
30+
digest_text TEXT,
31+
digest_json TEXT DEFAULT '{}',
32+
created_at INTEGER DEFAULT (unixepoch()),
33+
closed_at INTEGER,
34+
retention_policy TEXT DEFAULT 'default',
35+
importance_score REAL DEFAULT 0.5
36+
);
37+
CREATE TABLE anchors (
38+
anchor_id TEXT PRIMARY KEY,
39+
frame_id TEXT NOT NULL,
40+
type TEXT NOT NULL,
41+
text TEXT NOT NULL,
42+
priority INTEGER DEFAULT 0,
43+
created_at INTEGER DEFAULT (unixepoch())
44+
);
45+
CREATE TABLE events (
46+
event_id TEXT PRIMARY KEY,
47+
frame_id TEXT NOT NULL,
48+
event_type TEXT NOT NULL,
49+
payload TEXT DEFAULT '{}',
50+
ts INTEGER DEFAULT (unixepoch())
51+
);
52+
`);
53+
54+
// Insert a frame for "today" with project_id='default' (CLI fallback)
55+
const now = Math.floor(Date.now() / 1000);
56+
db.prepare(
57+
`INSERT INTO frames (frame_id, run_id, project_id, type, name, state, created_at)
58+
VALUES ('f1', 'r1', 'default', 'task', 'test-digest-frame', 'completed', ?)`
59+
).run(now - 60);
60+
db.prepare(
61+
`INSERT INTO anchors (anchor_id, frame_id, type, text, priority, created_at)
62+
VALUES ('a1', 'f1', 'DECISION', 'Test decision anchor', 5, ?)`
63+
).run(now - 60);
64+
db.close();
65+
}
66+
67+
describe('digest CLI command', () => {
68+
let tmpDir: string;
69+
let originalCwd: string;
70+
71+
beforeEach(() => {
72+
tmpDir = mkdtempSync(join(tmpdir(), 'sm-digest-test-'));
73+
originalCwd = process.cwd();
74+
setupTestProject(tmpDir);
75+
process.chdir(tmpDir);
76+
});
77+
78+
afterEach(() => {
79+
process.chdir(originalCwd);
80+
});
81+
82+
it('creates the digest command with correct name', () => {
83+
const cmd = createDigestCommands();
84+
expect(cmd.name()).toBe('digest');
85+
});
86+
87+
it('rejects invalid period argument', async () => {
88+
const program = new Command();
89+
program.addCommand(createDigestCommands());
90+
program.exitOverride();
91+
92+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
93+
throw new Error('process.exit called');
94+
});
95+
96+
try {
97+
await program.parseAsync(['node', 'stackmemory', 'digest', 'invalid']);
98+
} catch {
99+
// Expected — process.exit or exitOverride throws
100+
}
101+
102+
exitSpy.mockRestore();
103+
});
104+
105+
it('writes today.md to .stackmemory/', async () => {
106+
const program = new Command();
107+
program.addCommand(createDigestCommands());
108+
109+
await program.parseAsync(['node', 'stackmemory', 'digest', 'today']);
110+
111+
const outputPath = join(tmpDir, '.stackmemory', 'today.md');
112+
expect(existsSync(outputPath)).toBe(true);
113+
114+
const content = readFileSync(outputPath, 'utf8');
115+
expect(content).toContain('test-digest-frame');
116+
expect(content).toContain('DECISION: Test decision anchor');
117+
});
118+
119+
it('writes yesterday.md to .stackmemory/', async () => {
120+
const program = new Command();
121+
program.addCommand(createDigestCommands());
122+
123+
await program.parseAsync(['node', 'stackmemory', 'digest', 'yesterday']);
124+
125+
const outputPath = join(tmpDir, '.stackmemory', 'yesterday.md');
126+
expect(existsSync(outputPath)).toBe(true);
127+
});
128+
129+
it('writes week.md to .stackmemory/', async () => {
130+
const program = new Command();
131+
program.addCommand(createDigestCommands());
132+
133+
await program.parseAsync(['node', 'stackmemory', 'digest', 'week']);
134+
135+
const outputPath = join(tmpDir, '.stackmemory', 'week.md');
136+
expect(existsSync(outputPath)).toBe(true);
137+
138+
const content = readFileSync(outputPath, 'utf8');
139+
expect(content).toContain('test-digest-frame');
140+
});
141+
142+
it('supports custom output path with --output', async () => {
143+
const customPath = join(tmpDir, 'custom-digest.md');
144+
const program = new Command();
145+
program.addCommand(createDigestCommands());
146+
147+
await program.parseAsync([
148+
'node',
149+
'stackmemory',
150+
'digest',
151+
'today',
152+
'--output',
153+
customPath,
154+
]);
155+
156+
expect(existsSync(customPath)).toBe(true);
157+
const content = readFileSync(customPath, 'utf8');
158+
expect(content).toContain('test-digest-frame');
159+
});
160+
});

src/cli/commands/setup.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,44 @@ export function createDoctorCommand(): Command {
292292
});
293293
}
294294

295-
// 5. Check environment variables
295+
// 5. Check MCP tool definitions
296+
try {
297+
const { MCPToolDefinitions } =
298+
await import('../../integrations/mcp/tool-definitions.js');
299+
const toolDefs = new MCPToolDefinitions();
300+
const allTools = toolDefs.getAllToolDefinitions();
301+
const toolNames = allTools.map((t: { name: string }) => t.name);
302+
const expectedTools = [
303+
'sm_digest',
304+
'cord_spawn',
305+
'team_search',
306+
'get_context',
307+
'create_task',
308+
];
309+
const missing = expectedTools.filter((t) => !toolNames.includes(t));
310+
311+
if (missing.length === 0) {
312+
results.push({
313+
name: 'MCP Tools',
314+
status: 'ok',
315+
message: `${allTools.length} tool definitions loaded`,
316+
});
317+
} else {
318+
results.push({
319+
name: 'MCP Tools',
320+
status: 'warn',
321+
message: `${allTools.length} tools loaded, missing: ${missing.join(', ')}`,
322+
});
323+
}
324+
} catch (error) {
325+
results.push({
326+
name: 'MCP Tools',
327+
status: 'error',
328+
message: `Failed to load tool definitions: ${(error as Error).message}`,
329+
});
330+
}
331+
332+
// 6. Check environment variables
296333
const envChecks = [
297334
{ key: 'LINEAR_API_KEY', name: 'Linear API Key', optional: true },
298335
];
@@ -316,7 +353,7 @@ export function createDoctorCommand(): Command {
316353
// Skip optional env vars that aren't set
317354
}
318355

319-
// 6. Check file permissions
356+
// 7. Check file permissions
320357
const homeStackmemory = join(homedir(), '.stackmemory');
321358
if (existsSync(homeStackmemory)) {
322359
try {

0 commit comments

Comments
 (0)