anvil-dev-framework 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Tests command parsing and execution for init, observe, search, and get.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
8
|
+
import { unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
|
|
12
|
+
// Import command handlers and parsers
|
|
13
|
+
import { handleInit, parseInitArgs } from '../commands/init';
|
|
14
|
+
import { handleObserve, parseObserveArgs } from '../commands/observe';
|
|
15
|
+
import { handleSearch, parseSearchArgs } from '../commands/search';
|
|
16
|
+
import { handleGet, parseGetArgs } from '../commands/get';
|
|
17
|
+
import { handleCheckpoint, parseCheckpointArgs } from '../commands/checkpoint';
|
|
18
|
+
import { AnvilMemoryDb } from '../db';
|
|
19
|
+
import type { Observation, Checkpoint } from '../types';
|
|
20
|
+
|
|
21
|
+
// Type definitions for command result data
|
|
22
|
+
interface InitResultData {
|
|
23
|
+
created?: boolean;
|
|
24
|
+
version?: number;
|
|
25
|
+
integrityOk?: boolean;
|
|
26
|
+
path?: string;
|
|
27
|
+
migrated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ObserveResultData extends Observation {}
|
|
31
|
+
|
|
32
|
+
interface SearchResultData {
|
|
33
|
+
count: number;
|
|
34
|
+
results: Array<{
|
|
35
|
+
type: string;
|
|
36
|
+
title: string;
|
|
37
|
+
project?: string;
|
|
38
|
+
concepts?: string[];
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface GetResultData {
|
|
43
|
+
id: number;
|
|
44
|
+
title?: string;
|
|
45
|
+
project?: string;
|
|
46
|
+
level?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CheckpointResultData {
|
|
50
|
+
checkpoint?: Checkpoint;
|
|
51
|
+
observation?: Observation;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Test database path
|
|
55
|
+
const TEST_DB_DIR = join(tmpdir(), 'anvil-memory-cmd-tests');
|
|
56
|
+
const getTestDbPath = () => join(TEST_DB_DIR, `cmd-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
57
|
+
|
|
58
|
+
// Helper to clean up database files
|
|
59
|
+
function cleanupDb(path: string) {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(path)) unlinkSync(path);
|
|
62
|
+
if (existsSync(`${path}-wal`)) unlinkSync(`${path}-wal`);
|
|
63
|
+
if (existsSync(`${path}-shm`)) unlinkSync(`${path}-shm`);
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore cleanup errors
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('init command', () => {
|
|
70
|
+
let testDbPath: string;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
74
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
testDbPath = getTestDbPath();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
cleanupDb(testDbPath);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('parseInitArgs parses --path option', () => {
|
|
84
|
+
const opts = parseInitArgs(['--path', '/tmp/test.db']);
|
|
85
|
+
expect(opts.path).toBe('/tmp/test.db');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('parseInitArgs parses -p short option', () => {
|
|
89
|
+
const opts = parseInitArgs(['-p', '/tmp/test.db']);
|
|
90
|
+
expect(opts.path).toBe('/tmp/test.db');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('parseInitArgs parses --force option', () => {
|
|
94
|
+
const opts = parseInitArgs(['--force']);
|
|
95
|
+
expect(opts.force).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('parseInitArgs ignores --json flag', () => {
|
|
99
|
+
const opts = parseInitArgs(['--json', '--path', '/tmp/test.db']);
|
|
100
|
+
expect(opts.path).toBe('/tmp/test.db');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('handleInit creates new database', async () => {
|
|
104
|
+
const result = await handleInit(['--path', testDbPath]);
|
|
105
|
+
const data = result.data as InitResultData;
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.message).toContain('Created');
|
|
109
|
+
expect(data?.created).toBe(true);
|
|
110
|
+
expect(data?.version).toBe(1);
|
|
111
|
+
expect(existsSync(testDbPath)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('handleInit returns existing database info', async () => {
|
|
115
|
+
// Create database first
|
|
116
|
+
await handleInit(['--path', testDbPath]);
|
|
117
|
+
|
|
118
|
+
// Try to init again
|
|
119
|
+
const result = await handleInit(['--path', testDbPath]);
|
|
120
|
+
const data = result.data as InitResultData;
|
|
121
|
+
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
expect(result.message).toContain('already exists');
|
|
124
|
+
expect(data?.version).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('handleInit with --force reinitializes', async () => {
|
|
128
|
+
// Create database first
|
|
129
|
+
await handleInit(['--path', testDbPath]);
|
|
130
|
+
|
|
131
|
+
// Force reinit
|
|
132
|
+
const result = await handleInit(['--path', testDbPath, '--force']);
|
|
133
|
+
const data = result.data as InitResultData;
|
|
134
|
+
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
expect(data?.integrityOk).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('observe command', () => {
|
|
141
|
+
let testDbPath: string;
|
|
142
|
+
|
|
143
|
+
beforeAll(async () => {
|
|
144
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
145
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
testDbPath = getTestDbPath();
|
|
148
|
+
await handleInit(['--path', testDbPath]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterAll(() => {
|
|
152
|
+
cleanupDb(testDbPath);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('parseObserveArgs parses required options', () => {
|
|
156
|
+
const opts = parseObserveArgs(['--type', 'discovery', '--title', 'Test title']);
|
|
157
|
+
expect(opts.type).toBe('discovery');
|
|
158
|
+
expect(opts.title).toBe('Test title');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('parseObserveArgs parses short options', () => {
|
|
162
|
+
const opts = parseObserveArgs(['-t', 'bugfix', '--title', 'Fix bug', '-c', 'Bug content']);
|
|
163
|
+
expect(opts.type).toBe('bugfix');
|
|
164
|
+
expect(opts.content).toBe('Bug content');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('parseObserveArgs parses files and concepts', () => {
|
|
168
|
+
const opts = parseObserveArgs([
|
|
169
|
+
'--type', 'feature',
|
|
170
|
+
'--title', 'New feature',
|
|
171
|
+
'--files', 'src/a.ts,src/b.ts',
|
|
172
|
+
'--concepts', 'auth,security',
|
|
173
|
+
]);
|
|
174
|
+
expect(opts.files).toBe('src/a.ts,src/b.ts');
|
|
175
|
+
expect(opts.concepts).toBe('auth,security');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('handleObserve requires --type', async () => {
|
|
179
|
+
const result = await handleObserve(['--title', 'Test', '--path', testDbPath]);
|
|
180
|
+
|
|
181
|
+
expect(result.success).toBe(false);
|
|
182
|
+
expect(result.error).toContain('--type');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('handleObserve requires --title', async () => {
|
|
186
|
+
const result = await handleObserve(['--type', 'discovery', '--path', testDbPath]);
|
|
187
|
+
|
|
188
|
+
expect(result.success).toBe(false);
|
|
189
|
+
expect(result.error).toContain('--title');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('handleObserve validates type', async () => {
|
|
193
|
+
const result = await handleObserve(['--type', 'invalid', '--title', 'Test', '--path', testDbPath]);
|
|
194
|
+
|
|
195
|
+
expect(result.success).toBe(false);
|
|
196
|
+
expect(result.error).toContain('Invalid type');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('handleObserve creates observation', async () => {
|
|
200
|
+
const result = await handleObserve([
|
|
201
|
+
'--type', 'discovery',
|
|
202
|
+
'--title', 'Test observation',
|
|
203
|
+
'--content', 'Test content',
|
|
204
|
+
'--path', testDbPath,
|
|
205
|
+
]);
|
|
206
|
+
const data = result.data as ObserveResultData;
|
|
207
|
+
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
expect(data?.id).toBeDefined();
|
|
210
|
+
expect(data?.type).toBe('discovery');
|
|
211
|
+
expect(data?.title).toBe('Test observation');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('handleObserve handles all valid types', async () => {
|
|
215
|
+
const types = [
|
|
216
|
+
'bugfix', 'feature', 'refactor', 'discovery', 'decision',
|
|
217
|
+
'change', 'checkpoint', 'ralph_iteration', 'handoff',
|
|
218
|
+
'shard', 'linear_sync', 'session_request',
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
for (const type of types) {
|
|
222
|
+
const result = await handleObserve([
|
|
223
|
+
'--type', type,
|
|
224
|
+
'--title', `Test ${type}`,
|
|
225
|
+
'--path', testDbPath,
|
|
226
|
+
]);
|
|
227
|
+
const data = result.data as ObserveResultData;
|
|
228
|
+
expect(result.success).toBe(true);
|
|
229
|
+
expect(data?.type).toBe(type);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('handleObserve stores files as array', async () => {
|
|
234
|
+
const result = await handleObserve([
|
|
235
|
+
'--type', 'feature',
|
|
236
|
+
'--title', 'With files',
|
|
237
|
+
'--files', 'src/a.ts,src/b.ts',
|
|
238
|
+
'--path', testDbPath,
|
|
239
|
+
]);
|
|
240
|
+
const data = result.data as ObserveResultData;
|
|
241
|
+
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
expect(data?.files).toEqual(['src/a.ts', 'src/b.ts']);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('handleObserve stores concepts as array', async () => {
|
|
247
|
+
const result = await handleObserve([
|
|
248
|
+
'--type', 'discovery',
|
|
249
|
+
'--title', 'With concepts',
|
|
250
|
+
'--concepts', 'database,testing,fts5',
|
|
251
|
+
'--path', testDbPath,
|
|
252
|
+
]);
|
|
253
|
+
const data = result.data as ObserveResultData;
|
|
254
|
+
|
|
255
|
+
expect(result.success).toBe(true);
|
|
256
|
+
expect(data?.concepts).toEqual(['database', 'testing', 'fts5']);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('handleObserve parses work tokens', async () => {
|
|
260
|
+
const result = await handleObserve([
|
|
261
|
+
'--type', 'feature',
|
|
262
|
+
'--title', 'With tokens',
|
|
263
|
+
'--tokens', '5000',
|
|
264
|
+
'--path', testDbPath,
|
|
265
|
+
]);
|
|
266
|
+
const data = result.data as ObserveResultData;
|
|
267
|
+
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
expect(data?.work_tokens).toBe(5000);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('search command', () => {
|
|
274
|
+
let testDbPath: string;
|
|
275
|
+
|
|
276
|
+
beforeAll(async () => {
|
|
277
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
278
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
testDbPath = getTestDbPath();
|
|
281
|
+
await handleInit(['--path', testDbPath]);
|
|
282
|
+
|
|
283
|
+
// Add test observations
|
|
284
|
+
await handleObserve([
|
|
285
|
+
'--type', 'discovery',
|
|
286
|
+
'--title', 'SQLite FTS5 implementation',
|
|
287
|
+
'--content', 'Full-text search with Porter stemmer',
|
|
288
|
+
'--path', testDbPath,
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
await handleObserve([
|
|
292
|
+
'--type', 'bugfix',
|
|
293
|
+
'--title', 'Fixed database connection',
|
|
294
|
+
'--content', 'Resolved connection leak issue',
|
|
295
|
+
'--project', 'test-project',
|
|
296
|
+
'--path', testDbPath,
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
await handleObserve([
|
|
300
|
+
'--type', 'feature',
|
|
301
|
+
'--title', 'Authentication system',
|
|
302
|
+
'--content', 'JWT-based auth implementation',
|
|
303
|
+
'--project', 'auth-project',
|
|
304
|
+
'--path', testDbPath,
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterAll(() => {
|
|
309
|
+
cleanupDb(testDbPath);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('parseSearchArgs parses query from positional', () => {
|
|
313
|
+
const opts = parseSearchArgs(['database', 'search']);
|
|
314
|
+
expect(opts.query).toBe('database search');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('parseSearchArgs parses --limit option', () => {
|
|
318
|
+
const opts = parseSearchArgs(['test', '--limit', '5']);
|
|
319
|
+
expect(opts.limit).toBe(5);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('parseSearchArgs parses --type filter', () => {
|
|
323
|
+
const opts = parseSearchArgs(['test', '--type', 'bugfix']);
|
|
324
|
+
expect(opts.type).toBe('bugfix');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('parseSearchArgs parses --project filter', () => {
|
|
328
|
+
const opts = parseSearchArgs(['test', '--project', 'myproject']);
|
|
329
|
+
expect(opts.project).toBe('myproject');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('handleSearch requires query', async () => {
|
|
333
|
+
const result = await handleSearch(['--path', testDbPath]);
|
|
334
|
+
|
|
335
|
+
expect(result.success).toBe(false);
|
|
336
|
+
expect(result.error).toContain('Missing search query');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('handleSearch finds matching observations', async () => {
|
|
340
|
+
const result = await handleSearch(['FTS5', '--path', testDbPath]);
|
|
341
|
+
const data = result.data as SearchResultData;
|
|
342
|
+
|
|
343
|
+
expect(result.success).toBe(true);
|
|
344
|
+
expect(data?.count).toBeGreaterThan(0);
|
|
345
|
+
expect(data?.results[0]!.title).toContain('FTS5');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('handleSearch respects limit', async () => {
|
|
349
|
+
const result = await handleSearch(['database', '--limit', '1', '--path', testDbPath]);
|
|
350
|
+
const data = result.data as SearchResultData;
|
|
351
|
+
|
|
352
|
+
expect(result.success).toBe(true);
|
|
353
|
+
expect(data?.results.length).toBeLessThanOrEqual(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('handleSearch filters by type', async () => {
|
|
357
|
+
const result = await handleSearch(['connection', '--type', 'bugfix', '--path', testDbPath]);
|
|
358
|
+
const data = result.data as SearchResultData;
|
|
359
|
+
|
|
360
|
+
expect(result.success).toBe(true);
|
|
361
|
+
if (data?.count > 0) {
|
|
362
|
+
expect(data.results.every((r) => r.type === 'bugfix')).toBe(true);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('handleSearch filters by project', async () => {
|
|
367
|
+
const result = await handleSearch(['auth', '--project', 'auth-project', '--path', testDbPath]);
|
|
368
|
+
const data = result.data as SearchResultData;
|
|
369
|
+
|
|
370
|
+
expect(result.success).toBe(true);
|
|
371
|
+
if (data?.count > 0) {
|
|
372
|
+
expect(data.results.every((r) => r.project === 'auth-project')).toBe(true);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('handleSearch returns empty for no matches', async () => {
|
|
377
|
+
const result = await handleSearch(['xyznonexistent456', '--path', testDbPath]);
|
|
378
|
+
const data = result.data as SearchResultData;
|
|
379
|
+
|
|
380
|
+
expect(result.success).toBe(true);
|
|
381
|
+
expect(data?.count).toBe(0);
|
|
382
|
+
expect(data?.results).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('get command', () => {
|
|
387
|
+
let testDbPath: string;
|
|
388
|
+
let observationId: number;
|
|
389
|
+
let sessionId: number;
|
|
390
|
+
let checkpointId: number;
|
|
391
|
+
|
|
392
|
+
beforeAll(async () => {
|
|
393
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
394
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
testDbPath = getTestDbPath();
|
|
397
|
+
await handleInit(['--path', testDbPath]);
|
|
398
|
+
|
|
399
|
+
// Create test records
|
|
400
|
+
const obsResult = await handleObserve([
|
|
401
|
+
'--type', 'discovery',
|
|
402
|
+
'--title', 'Test for get',
|
|
403
|
+
'--content', 'Content to retrieve',
|
|
404
|
+
'--path', testDbPath,
|
|
405
|
+
]);
|
|
406
|
+
observationId = (obsResult.data as ObserveResultData)?.id;
|
|
407
|
+
|
|
408
|
+
// Create session and checkpoint directly
|
|
409
|
+
const db = new AnvilMemoryDb(testDbPath);
|
|
410
|
+
const session = db.createSession({
|
|
411
|
+
started_at: new Date().toISOString(),
|
|
412
|
+
project: 'test-project',
|
|
413
|
+
});
|
|
414
|
+
sessionId = session.id!;
|
|
415
|
+
|
|
416
|
+
const checkpoint = db.createCheckpoint({
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
level: 'L1',
|
|
419
|
+
context_percent: 50,
|
|
420
|
+
});
|
|
421
|
+
checkpointId = checkpoint.id!;
|
|
422
|
+
|
|
423
|
+
db.close();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
afterAll(() => {
|
|
427
|
+
cleanupDb(testDbPath);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test('parseGetArgs parses ID from positional', () => {
|
|
431
|
+
const opts = parseGetArgs(['123']);
|
|
432
|
+
expect('id' in opts && opts.id).toBe(123);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('parseGetArgs parses --type option', () => {
|
|
436
|
+
const opts = parseGetArgs(['1', '--type', 'session']);
|
|
437
|
+
expect('recordType' in opts && opts.recordType).toBe('session');
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('parseGetArgs defaults to observation type', () => {
|
|
441
|
+
const opts = parseGetArgs(['1']);
|
|
442
|
+
expect('recordType' in opts && opts.recordType).toBe('observation');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('parseGetArgs returns error for missing ID', () => {
|
|
446
|
+
const opts = parseGetArgs([]);
|
|
447
|
+
expect('error' in opts).toBe(true);
|
|
448
|
+
expect('error' in opts && opts.error).toContain('Missing ID');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('parseGetArgs returns error for invalid ID', () => {
|
|
452
|
+
const opts = parseGetArgs(['abc']);
|
|
453
|
+
expect('error' in opts).toBe(true);
|
|
454
|
+
expect('error' in opts && opts.error).toContain('Invalid ID');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('parseGetArgs returns error for invalid type', () => {
|
|
458
|
+
const opts = parseGetArgs(['1', '--type', 'invalid']);
|
|
459
|
+
expect('error' in opts).toBe(true);
|
|
460
|
+
expect('error' in opts && opts.error).toContain('Invalid type');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('handleGet retrieves observation', async () => {
|
|
464
|
+
const result = await handleGet([String(observationId), '--path', testDbPath]);
|
|
465
|
+
const data = result.data as GetResultData;
|
|
466
|
+
|
|
467
|
+
expect(result.success).toBe(true);
|
|
468
|
+
expect(data?.id).toBe(observationId);
|
|
469
|
+
expect(data?.title).toBe('Test for get');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('handleGet retrieves session', async () => {
|
|
473
|
+
const result = await handleGet([String(sessionId), '--type', 'session', '--path', testDbPath]);
|
|
474
|
+
const data = result.data as GetResultData;
|
|
475
|
+
|
|
476
|
+
expect(result.success).toBe(true);
|
|
477
|
+
expect(data?.id).toBe(sessionId);
|
|
478
|
+
expect(data?.project).toBe('test-project');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test('handleGet retrieves checkpoint', async () => {
|
|
482
|
+
const result = await handleGet([String(checkpointId), '--type', 'checkpoint', '--path', testDbPath]);
|
|
483
|
+
const data = result.data as GetResultData;
|
|
484
|
+
|
|
485
|
+
expect(result.success).toBe(true);
|
|
486
|
+
expect(data?.id).toBe(checkpointId);
|
|
487
|
+
expect(data?.level).toBe('L1');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('handleGet returns error for non-existent ID', async () => {
|
|
491
|
+
const result = await handleGet(['999999', '--path', testDbPath]);
|
|
492
|
+
|
|
493
|
+
expect(result.success).toBe(false);
|
|
494
|
+
expect(result.error).toContain('not found');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('handleGet returns error for non-existent session', async () => {
|
|
498
|
+
const result = await handleGet(['999999', '--type', 'session', '--path', testDbPath]);
|
|
499
|
+
|
|
500
|
+
expect(result.success).toBe(false);
|
|
501
|
+
expect(result.error).toContain('session');
|
|
502
|
+
expect(result.error).toContain('not found');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('checkpoint command', () => {
|
|
507
|
+
let testDbPath: string;
|
|
508
|
+
|
|
509
|
+
beforeAll(async () => {
|
|
510
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
511
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
512
|
+
}
|
|
513
|
+
testDbPath = getTestDbPath();
|
|
514
|
+
await handleInit(['--path', testDbPath]);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
afterAll(() => {
|
|
518
|
+
cleanupDb(testDbPath);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('parseCheckpointArgs parses required options', () => {
|
|
522
|
+
const opts = parseCheckpointArgs(['--level', 'L2', '--percent', '85']);
|
|
523
|
+
expect(opts.level).toBe('L2');
|
|
524
|
+
expect(opts.percent).toBe('85');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('parseCheckpointArgs parses short options', () => {
|
|
528
|
+
const opts = parseCheckpointArgs(['-l', 'L1', '-p', '70']);
|
|
529
|
+
expect(opts.level).toBe('L1');
|
|
530
|
+
expect(opts.percent).toBe('70');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('parseCheckpointArgs parses optional fields', () => {
|
|
534
|
+
const opts = parseCheckpointArgs([
|
|
535
|
+
'--level', 'L2',
|
|
536
|
+
'--percent', '85',
|
|
537
|
+
'--handoff', '.claude/handoffs/test.md',
|
|
538
|
+
'--task', 'Implementing feature',
|
|
539
|
+
'--progress', '50%',
|
|
540
|
+
'--linear', 'ANV-123',
|
|
541
|
+
]);
|
|
542
|
+
expect(opts.handoff).toBe('.claude/handoffs/test.md');
|
|
543
|
+
expect(opts.task).toBe('Implementing feature');
|
|
544
|
+
expect(opts.progress).toBe('50%');
|
|
545
|
+
expect(opts.linear).toBe('ANV-123');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('handleCheckpoint requires --level', async () => {
|
|
549
|
+
const result = await handleCheckpoint(['--percent', '85', '--path', testDbPath]);
|
|
550
|
+
|
|
551
|
+
expect(result.success).toBe(false);
|
|
552
|
+
expect(result.error).toContain('--level');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('handleCheckpoint requires --percent', async () => {
|
|
556
|
+
const result = await handleCheckpoint(['--level', 'L2', '--path', testDbPath]);
|
|
557
|
+
|
|
558
|
+
expect(result.success).toBe(false);
|
|
559
|
+
expect(result.error).toContain('--percent');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test('handleCheckpoint validates level', async () => {
|
|
563
|
+
const result = await handleCheckpoint(['--level', 'L5', '--percent', '85', '--path', testDbPath]);
|
|
564
|
+
|
|
565
|
+
expect(result.success).toBe(false);
|
|
566
|
+
expect(result.error).toContain('Invalid level');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('handleCheckpoint validates percent range', async () => {
|
|
570
|
+
const result = await handleCheckpoint(['--level', 'L2', '--percent', '150', '--path', testDbPath]);
|
|
571
|
+
|
|
572
|
+
expect(result.success).toBe(false);
|
|
573
|
+
expect(result.error).toContain('Invalid percent');
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('handleCheckpoint creates checkpoint record', async () => {
|
|
577
|
+
const result = await handleCheckpoint([
|
|
578
|
+
'--level', 'L2',
|
|
579
|
+
'--percent', '85',
|
|
580
|
+
'--path', testDbPath,
|
|
581
|
+
]);
|
|
582
|
+
const data = result.data as CheckpointResultData;
|
|
583
|
+
|
|
584
|
+
expect(result.success).toBe(true);
|
|
585
|
+
expect(result.message).toContain('L2');
|
|
586
|
+
expect(result.message).toContain('85%');
|
|
587
|
+
expect(data?.checkpoint?.level).toBe('L2');
|
|
588
|
+
expect(data?.checkpoint?.context_percent).toBe(85);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('handleCheckpoint creates linked observation with --task', async () => {
|
|
592
|
+
const result = await handleCheckpoint([
|
|
593
|
+
'--level', 'L2',
|
|
594
|
+
'--percent', '87',
|
|
595
|
+
'--task', 'Implementing OAuth callback',
|
|
596
|
+
'--progress', '70% complete',
|
|
597
|
+
'--linear', 'ANV-123',
|
|
598
|
+
'--path', testDbPath,
|
|
599
|
+
]);
|
|
600
|
+
const data = result.data as CheckpointResultData;
|
|
601
|
+
|
|
602
|
+
expect(result.success).toBe(true);
|
|
603
|
+
expect(data?.observation).toBeDefined();
|
|
604
|
+
expect(data?.observation?.type).toBe('checkpoint');
|
|
605
|
+
expect(data?.observation?.linear_issue).toBe('ANV-123');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test('handleCheckpoint accepts lowercase level', async () => {
|
|
609
|
+
const result = await handleCheckpoint([
|
|
610
|
+
'--level', 'l1',
|
|
611
|
+
'--percent', '70',
|
|
612
|
+
'--path', testDbPath,
|
|
613
|
+
]);
|
|
614
|
+
const data = result.data as CheckpointResultData;
|
|
615
|
+
|
|
616
|
+
expect(result.success).toBe(true);
|
|
617
|
+
expect(data?.checkpoint?.level).toBe('L1');
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('handleCheckpoint fails when database does not exist', async () => {
|
|
621
|
+
const result = await handleCheckpoint([
|
|
622
|
+
'--level', 'L2',
|
|
623
|
+
'--percent', '85',
|
|
624
|
+
'--path', '/tmp/nonexistent-db-12345.db',
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
expect(result.success).toBe(false);
|
|
628
|
+
expect(result.error).toContain('Database not found');
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe('Command error handling', () => {
|
|
633
|
+
test('observe fails when database does not exist', async () => {
|
|
634
|
+
const result = await handleObserve([
|
|
635
|
+
'--type', 'discovery',
|
|
636
|
+
'--title', 'Test',
|
|
637
|
+
'--path', '/tmp/nonexistent-db-12345.db',
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
expect(result.success).toBe(false);
|
|
641
|
+
expect(result.error).toContain('Database not found');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('search fails when database does not exist', async () => {
|
|
645
|
+
const result = await handleSearch(['query', '--path', '/tmp/nonexistent-db-12345.db']);
|
|
646
|
+
|
|
647
|
+
expect(result.success).toBe(false);
|
|
648
|
+
expect(result.error).toContain('Database not found');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('get fails when database does not exist', async () => {
|
|
652
|
+
const result = await handleGet(['1', '--path', '/tmp/nonexistent-db-12345.db']);
|
|
653
|
+
|
|
654
|
+
expect(result.success).toBe(false);
|
|
655
|
+
expect(result.error).toContain('Database not found');
|
|
656
|
+
});
|
|
657
|
+
});
|