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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for Anvil Memory hooks
|
|
3
|
+
*
|
|
4
|
+
* Tests hook execution as subprocesses with stdin/stdout.
|
|
5
|
+
* Validates JSON output structure, error handling, and graceful degradation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { AnvilMemoryDb } from '../db';
|
|
12
|
+
import { cleanupDb, getTestDbPath, ensureTestDir } from './test-utils';
|
|
13
|
+
|
|
14
|
+
// Project paths
|
|
15
|
+
const PROJECT_ROOT = join(__dirname, '../../../../..');
|
|
16
|
+
const HOOKS_DIR = join(PROJECT_ROOT, 'global/hooks');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runs a hook as a subprocess with given stdin input
|
|
20
|
+
*/
|
|
21
|
+
async function runHook(
|
|
22
|
+
hookName: string,
|
|
23
|
+
input: Record<string, unknown>,
|
|
24
|
+
dbPath?: string
|
|
25
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
26
|
+
const hookPath = join(HOOKS_DIR, hookName);
|
|
27
|
+
const inputJson = JSON.stringify(input);
|
|
28
|
+
|
|
29
|
+
// Set DB_PATH environment variable if provided
|
|
30
|
+
const env = { ...process.env };
|
|
31
|
+
if (dbPath) {
|
|
32
|
+
env.ANVIL_MEMORY_DB_PATH = dbPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const proc = Bun.spawn(['bun', 'run', hookPath], {
|
|
36
|
+
stdin: new Blob([inputJson]),
|
|
37
|
+
stdout: 'pipe',
|
|
38
|
+
stderr: 'pipe',
|
|
39
|
+
cwd: PROJECT_ROOT,
|
|
40
|
+
env,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const stdout = await new Response(proc.stdout).text();
|
|
44
|
+
const stderr = await new Response(proc.stderr).text();
|
|
45
|
+
await proc.exited;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
stdout: stdout.trim(),
|
|
49
|
+
stderr: stderr.trim(),
|
|
50
|
+
exitCode: proc.exitCode ?? 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('anvil_memory_observe hook', () => {
|
|
55
|
+
let testDbPath: string;
|
|
56
|
+
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
ensureTestDir();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
testDbPath = getTestDbPath('hook-observe');
|
|
63
|
+
const db = new AnvilMemoryDb(testDbPath);
|
|
64
|
+
db.init();
|
|
65
|
+
db.close();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
cleanupDb(testDbPath);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('outputs empty JSON for skipped tools', async () => {
|
|
73
|
+
const result = await runHook('anvil_memory_observe.ts', {
|
|
74
|
+
tool_name: 'Glob',
|
|
75
|
+
tool_result: 'Some file results',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.exitCode).toBe(0);
|
|
79
|
+
expect(result.stdout).toBe('{}');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('outputs empty JSON for empty results', async () => {
|
|
83
|
+
const result = await runHook('anvil_memory_observe.ts', {
|
|
84
|
+
tool_name: 'Write',
|
|
85
|
+
tool_result: '',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.exitCode).toBe(0);
|
|
89
|
+
expect(result.stdout).toBe('{}');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('outputs empty JSON for short results', async () => {
|
|
93
|
+
const result = await runHook('anvil_memory_observe.ts', {
|
|
94
|
+
tool_name: 'Write',
|
|
95
|
+
tool_result: 'too short',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.exitCode).toBe(0);
|
|
99
|
+
expect(result.stdout).toBe('{}');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('outputs empty JSON without database', async () => {
|
|
103
|
+
// Use non-existent db path
|
|
104
|
+
const result = await runHook('anvil_memory_observe.ts', {
|
|
105
|
+
tool_name: 'Write',
|
|
106
|
+
tool_input: { file_path: '/test/file.ts' },
|
|
107
|
+
tool_result: 'File written successfully to /test/file.ts. This is a significant result.',
|
|
108
|
+
}, '/tmp/nonexistent-db-abc123.db');
|
|
109
|
+
|
|
110
|
+
expect(result.exitCode).toBe(0);
|
|
111
|
+
expect(result.stdout).toBe('{}');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('handles malformed JSON input gracefully', async () => {
|
|
115
|
+
const hookPath = join(HOOKS_DIR, 'anvil_memory_observe.ts');
|
|
116
|
+
|
|
117
|
+
const proc = Bun.spawn(['bun', 'run', hookPath], {
|
|
118
|
+
stdin: new Blob(['not valid json']),
|
|
119
|
+
stdout: 'pipe',
|
|
120
|
+
stderr: 'pipe',
|
|
121
|
+
cwd: PROJECT_ROOT,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const stdout = await new Response(proc.stdout).text();
|
|
125
|
+
await proc.exited;
|
|
126
|
+
|
|
127
|
+
expect(proc.exitCode).toBe(0);
|
|
128
|
+
expect(stdout.trim()).toBe('{}');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('anvil_memory_session hook', () => {
|
|
133
|
+
let testDbPath: string;
|
|
134
|
+
|
|
135
|
+
beforeAll(() => {
|
|
136
|
+
ensureTestDir();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
// Create isolated test database for session hook tests
|
|
141
|
+
testDbPath = getTestDbPath('hook-session');
|
|
142
|
+
const db = new AnvilMemoryDb(testDbPath);
|
|
143
|
+
db.init();
|
|
144
|
+
db.close();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
cleanupDb(testDbPath);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('outputs valid JSON response', async () => {
|
|
152
|
+
// Use isolated test database
|
|
153
|
+
const result = await runHook('anvil_memory_session.ts', {}, testDbPath);
|
|
154
|
+
|
|
155
|
+
expect(result.exitCode).toBe(0);
|
|
156
|
+
// Should output valid JSON (either {} or {additionalContext: ...})
|
|
157
|
+
expect(() => JSON.parse(result.stdout)).not.toThrow();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('response has correct structure', async () => {
|
|
161
|
+
const result = await runHook('anvil_memory_session.ts', {}, testDbPath);
|
|
162
|
+
|
|
163
|
+
expect(result.exitCode).toBe(0);
|
|
164
|
+
const output = JSON.parse(result.stdout);
|
|
165
|
+
// Should be either empty or have additionalContext
|
|
166
|
+
if (Object.keys(output).length > 0) {
|
|
167
|
+
expect(output.additionalContext).toBeDefined();
|
|
168
|
+
expect(typeof output.additionalContext).toBe('string');
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('handles malformed JSON input gracefully', async () => {
|
|
173
|
+
const hookPath = join(HOOKS_DIR, 'anvil_memory_session.ts');
|
|
174
|
+
|
|
175
|
+
const env = { ...process.env, ANVIL_MEMORY_DB_PATH: testDbPath };
|
|
176
|
+
const proc = Bun.spawn(['bun', 'run', hookPath], {
|
|
177
|
+
stdin: new Blob(['{ invalid json']),
|
|
178
|
+
stdout: 'pipe',
|
|
179
|
+
stderr: 'pipe',
|
|
180
|
+
cwd: PROJECT_ROOT,
|
|
181
|
+
env,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const stdout = await new Response(proc.stdout).text();
|
|
185
|
+
await proc.exited;
|
|
186
|
+
|
|
187
|
+
expect(proc.exitCode).toBe(0);
|
|
188
|
+
// Should still output valid JSON (empty or with context)
|
|
189
|
+
expect(() => JSON.parse(stdout)).not.toThrow();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('anvil_memory_stop hook', () => {
|
|
194
|
+
let testDbPath: string;
|
|
195
|
+
|
|
196
|
+
beforeAll(() => {
|
|
197
|
+
ensureTestDir();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
testDbPath = getTestDbPath('hook-stop');
|
|
202
|
+
const db = new AnvilMemoryDb(testDbPath);
|
|
203
|
+
db.init();
|
|
204
|
+
db.close();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
cleanupDb(testDbPath);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('outputs empty JSON on success', async () => {
|
|
212
|
+
const result = await runHook('anvil_memory_stop.ts', {
|
|
213
|
+
duration_seconds: 300,
|
|
214
|
+
context_used_percent: 50,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(result.exitCode).toBe(0);
|
|
218
|
+
expect(result.stdout).toBe('{}');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('outputs empty JSON without database', async () => {
|
|
222
|
+
const result = await runHook('anvil_memory_stop.ts', {}, '/tmp/nonexistent-db-stop123.db');
|
|
223
|
+
|
|
224
|
+
expect(result.exitCode).toBe(0);
|
|
225
|
+
expect(result.stdout).toBe('{}');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('handles empty input', async () => {
|
|
229
|
+
const result = await runHook('anvil_memory_stop.ts', {});
|
|
230
|
+
|
|
231
|
+
expect(result.exitCode).toBe(0);
|
|
232
|
+
expect(result.stdout).toBe('{}');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('handles malformed JSON input gracefully', async () => {
|
|
236
|
+
const hookPath = join(HOOKS_DIR, 'anvil_memory_stop.ts');
|
|
237
|
+
|
|
238
|
+
const proc = Bun.spawn(['bun', 'run', hookPath], {
|
|
239
|
+
stdin: new Blob(['not json at all']),
|
|
240
|
+
stdout: 'pipe',
|
|
241
|
+
stderr: 'pipe',
|
|
242
|
+
cwd: PROJECT_ROOT,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const stdout = await new Response(proc.stdout).text();
|
|
246
|
+
await proc.exited;
|
|
247
|
+
|
|
248
|
+
expect(proc.exitCode).toBe(0);
|
|
249
|
+
expect(stdout.trim()).toBe('{}');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Hook helper functions', () => {
|
|
254
|
+
// These tests verify the internal logic by importing the modules directly
|
|
255
|
+
// We test the main execution flow in the subprocess tests above
|
|
256
|
+
|
|
257
|
+
test('hooks exist at expected paths', () => {
|
|
258
|
+
expect(existsSync(join(HOOKS_DIR, 'anvil_memory_observe.ts'))).toBe(true);
|
|
259
|
+
expect(existsSync(join(HOOKS_DIR, 'anvil_memory_session.ts'))).toBe(true);
|
|
260
|
+
expect(existsSync(join(HOOKS_DIR, 'anvil_memory_stop.ts'))).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('hooks are executable', async () => {
|
|
264
|
+
// Each hook should run and exit cleanly with empty input
|
|
265
|
+
const hooks = ['anvil_memory_observe.ts', 'anvil_memory_session.ts', 'anvil_memory_stop.ts'];
|
|
266
|
+
|
|
267
|
+
for (const hook of hooks) {
|
|
268
|
+
const result = await runHook(hook, {});
|
|
269
|
+
expect(result.exitCode).toBe(0);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tests for Anvil Memory Database
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Query plans use indexes appropriately
|
|
6
|
+
* - Operations complete in <100ms
|
|
7
|
+
* - FTS5 queries are optimized
|
|
8
|
+
* - System scales to 10k+ observations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
12
|
+
import { AnvilMemoryDb } from '../db';
|
|
13
|
+
import {
|
|
14
|
+
cleanupDb,
|
|
15
|
+
getTestDbPath,
|
|
16
|
+
ensureTestDir,
|
|
17
|
+
measureTime,
|
|
18
|
+
randomType,
|
|
19
|
+
randomWords,
|
|
20
|
+
} from './test-utils';
|
|
21
|
+
|
|
22
|
+
describe('Query Plan Analysis', () => {
|
|
23
|
+
let db: AnvilMemoryDb;
|
|
24
|
+
let testDbPath: string;
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
ensureTestDir();
|
|
28
|
+
testDbPath = getTestDbPath('perf-query');
|
|
29
|
+
db = new AnvilMemoryDb(testDbPath);
|
|
30
|
+
db.init();
|
|
31
|
+
|
|
32
|
+
// Add sample data for query plan analysis
|
|
33
|
+
for (let i = 0; i < 100; i++) {
|
|
34
|
+
db.createObservation({
|
|
35
|
+
timestamp: new Date(Date.now() - i * 60000).toISOString(),
|
|
36
|
+
type: randomType(),
|
|
37
|
+
title: `Observation ${i}: ${randomWords(3)}`,
|
|
38
|
+
content: randomWords(20),
|
|
39
|
+
project: i % 3 === 0 ? 'project-a' : 'project-b',
|
|
40
|
+
files: [`file${i}.ts`],
|
|
41
|
+
concepts: [randomWords(1), randomWords(1)],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
db.close();
|
|
48
|
+
cleanupDb(testDbPath);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('getObservation uses PRIMARY KEY index', () => {
|
|
52
|
+
const rawDb = db.getDb();
|
|
53
|
+
const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations WHERE id = ?').all(1);
|
|
54
|
+
const planStr = JSON.stringify(plan);
|
|
55
|
+
|
|
56
|
+
// Should use primary key (SEARCH using INTEGER PRIMARY KEY)
|
|
57
|
+
expect(planStr).toMatch(/SEARCH|PRIMARY/i);
|
|
58
|
+
expect(planStr).not.toMatch(/SCAN/i);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('getRecentObservations uses timestamp index', () => {
|
|
62
|
+
const rawDb = db.getDb();
|
|
63
|
+
const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations ORDER BY timestamp DESC LIMIT ?').all(20);
|
|
64
|
+
|
|
65
|
+
// Should use idx_observations_timestamp or be efficient
|
|
66
|
+
// Note: SQLite may choose SCAN if table is small, which is actually faster
|
|
67
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('getRecentObservations with type filter uses index', () => {
|
|
71
|
+
const rawDb = db.getDb();
|
|
72
|
+
const plan = rawDb.query("EXPLAIN QUERY PLAN SELECT * FROM observations WHERE type IN (?, ?) ORDER BY timestamp DESC LIMIT ?").all('bugfix', 'feature', 20);
|
|
73
|
+
|
|
74
|
+
// Query plan should exist; actual index usage depends on table size
|
|
75
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('getRecentObservations with project filter uses index', () => {
|
|
79
|
+
const rawDb = db.getDb();
|
|
80
|
+
const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM observations WHERE project = ? ORDER BY timestamp DESC LIMIT ?').all('project-a', 20);
|
|
81
|
+
|
|
82
|
+
// Query plan should exist; actual index usage depends on table size
|
|
83
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('searchObservations FTS5 query is optimized', () => {
|
|
87
|
+
const rawDb = db.getDb();
|
|
88
|
+
const plan = rawDb.query(`
|
|
89
|
+
EXPLAIN QUERY PLAN
|
|
90
|
+
SELECT o.* FROM observations o
|
|
91
|
+
JOIN observations_fts fts ON o.id = fts.rowid
|
|
92
|
+
WHERE observations_fts MATCH ?
|
|
93
|
+
ORDER BY rank
|
|
94
|
+
LIMIT ?
|
|
95
|
+
`).all('database', 20);
|
|
96
|
+
|
|
97
|
+
// FTS5 queries should use virtual table efficiently
|
|
98
|
+
expect(plan.length).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('getSession uses PRIMARY KEY index', () => {
|
|
102
|
+
const rawDb = db.getDb();
|
|
103
|
+
const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM sessions WHERE id = ?').all(1);
|
|
104
|
+
const planStr = JSON.stringify(plan);
|
|
105
|
+
|
|
106
|
+
expect(planStr).toMatch(/SEARCH|PRIMARY/i);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('getCheckpoint uses PRIMARY KEY index', () => {
|
|
110
|
+
const rawDb = db.getDb();
|
|
111
|
+
const plan = rawDb.query('EXPLAIN QUERY PLAN SELECT * FROM checkpoints WHERE id = ?').all(1);
|
|
112
|
+
const planStr = JSON.stringify(plan);
|
|
113
|
+
|
|
114
|
+
expect(planStr).toMatch(/SEARCH|PRIMARY/i);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Operation Timing (<100ms target)', () => {
|
|
119
|
+
let db: AnvilMemoryDb;
|
|
120
|
+
let testDbPath: string;
|
|
121
|
+
|
|
122
|
+
beforeAll(() => {
|
|
123
|
+
ensureTestDir();
|
|
124
|
+
testDbPath = getTestDbPath('perf-timing');
|
|
125
|
+
db = new AnvilMemoryDb(testDbPath);
|
|
126
|
+
db.init();
|
|
127
|
+
|
|
128
|
+
// Add 1000 observations for timing tests
|
|
129
|
+
for (let i = 0; i < 1000; i++) {
|
|
130
|
+
db.createObservation({
|
|
131
|
+
timestamp: new Date(Date.now() - i * 60000).toISOString(),
|
|
132
|
+
type: randomType(),
|
|
133
|
+
title: `Observation ${i}: ${randomWords(3)}`,
|
|
134
|
+
content: randomWords(50),
|
|
135
|
+
project: `project-${i % 10}`,
|
|
136
|
+
files: [`src/file${i}.ts`, `test/file${i}.test.ts`],
|
|
137
|
+
concepts: [randomWords(1), randomWords(1), randomWords(1)],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterAll(() => {
|
|
143
|
+
db.close();
|
|
144
|
+
cleanupDb(testDbPath);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('createObservation completes in <100ms', () => {
|
|
148
|
+
const { timeMs } = measureTime(() => {
|
|
149
|
+
db.createObservation({
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
type: 'discovery',
|
|
152
|
+
title: 'Test observation',
|
|
153
|
+
content: 'Test content for timing',
|
|
154
|
+
project: 'test-project',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(timeMs).toBeLessThan(100);
|
|
159
|
+
console.log(`createObservation: ${timeMs.toFixed(2)}ms`);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('getObservation completes in <100ms', () => {
|
|
163
|
+
const { timeMs } = measureTime(() => {
|
|
164
|
+
db.getObservation(500);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(timeMs).toBeLessThan(100);
|
|
168
|
+
console.log(`getObservation: ${timeMs.toFixed(2)}ms`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('searchObservations completes in <100ms', () => {
|
|
172
|
+
const { timeMs } = measureTime(() => {
|
|
173
|
+
db.searchObservations('database optimization', 20);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(timeMs).toBeLessThan(100);
|
|
177
|
+
console.log(`searchObservations: ${timeMs.toFixed(2)}ms`);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('getRecentObservations completes in <100ms', () => {
|
|
181
|
+
const { timeMs } = measureTime(() => {
|
|
182
|
+
db.getRecentObservations(50);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(timeMs).toBeLessThan(100);
|
|
186
|
+
console.log(`getRecentObservations: ${timeMs.toFixed(2)}ms`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('getRecentObservations with filters completes in <100ms', () => {
|
|
190
|
+
const { timeMs } = measureTime(() => {
|
|
191
|
+
db.getRecentObservations(50, ['bugfix', 'feature'], 'project-1');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(timeMs).toBeLessThan(100);
|
|
195
|
+
console.log(`getRecentObservations (filtered): ${timeMs.toFixed(2)}ms`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('createSession completes in <100ms', () => {
|
|
199
|
+
const { timeMs } = measureTime(() => {
|
|
200
|
+
db.createSession({
|
|
201
|
+
started_at: new Date().toISOString(),
|
|
202
|
+
project: 'test-project',
|
|
203
|
+
branch: 'feature/test',
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(timeMs).toBeLessThan(100);
|
|
208
|
+
console.log(`createSession: ${timeMs.toFixed(2)}ms`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('createCheckpoint completes in <100ms', () => {
|
|
212
|
+
const { timeMs } = measureTime(() => {
|
|
213
|
+
db.createCheckpoint({
|
|
214
|
+
timestamp: new Date().toISOString(),
|
|
215
|
+
level: 'L1',
|
|
216
|
+
context_percent: 50,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(timeMs).toBeLessThan(100);
|
|
221
|
+
console.log(`createCheckpoint: ${timeMs.toFixed(2)}ms`);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('createPrompt completes in <100ms', () => {
|
|
225
|
+
const { timeMs } = measureTime(() => {
|
|
226
|
+
db.createPrompt({
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
content: 'Help me implement a new feature',
|
|
229
|
+
project: 'test-project',
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(timeMs).toBeLessThan(100);
|
|
234
|
+
console.log(`createPrompt: ${timeMs.toFixed(2)}ms`);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Scale Testing (10k+ observations)', () => {
|
|
239
|
+
let db: AnvilMemoryDb;
|
|
240
|
+
let testDbPath: string;
|
|
241
|
+
|
|
242
|
+
beforeAll(() => {
|
|
243
|
+
ensureTestDir();
|
|
244
|
+
testDbPath = getTestDbPath('perf-scale');
|
|
245
|
+
db = new AnvilMemoryDb(testDbPath);
|
|
246
|
+
db.init();
|
|
247
|
+
|
|
248
|
+
console.log('Creating 10,000 observations for scale test...');
|
|
249
|
+
const startTime = performance.now();
|
|
250
|
+
|
|
251
|
+
// Batch insert for faster setup
|
|
252
|
+
const rawDb = db.getDb();
|
|
253
|
+
rawDb.exec('BEGIN TRANSACTION');
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < 10000; i++) {
|
|
256
|
+
db.createObservation({
|
|
257
|
+
timestamp: new Date(Date.now() - i * 60000).toISOString(),
|
|
258
|
+
type: randomType(),
|
|
259
|
+
title: `Scale test observation ${i}: ${randomWords(3)}`,
|
|
260
|
+
content: randomWords(100),
|
|
261
|
+
project: `project-${i % 50}`,
|
|
262
|
+
files: [`src/module${i % 100}/file${i}.ts`],
|
|
263
|
+
concepts: [randomWords(1), randomWords(1)],
|
|
264
|
+
work_tokens: Math.floor(Math.random() * 1000),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Log progress every 1000
|
|
268
|
+
if ((i + 1) % 1000 === 0) {
|
|
269
|
+
console.log(` Created ${i + 1} observations...`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
rawDb.exec('COMMIT');
|
|
274
|
+
|
|
275
|
+
const setupTime = performance.now() - startTime;
|
|
276
|
+
console.log(`Setup complete in ${(setupTime / 1000).toFixed(2)}s`);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
afterAll(() => {
|
|
280
|
+
db.close();
|
|
281
|
+
cleanupDb(testDbPath);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('searchObservations at 10k scale completes in <100ms', () => {
|
|
285
|
+
const { result, timeMs } = measureTime(() => {
|
|
286
|
+
return db.searchObservations('database performance', 20);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(timeMs).toBeLessThan(100);
|
|
290
|
+
console.log(`searchObservations (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('getRecentObservations at 10k scale completes in <100ms', () => {
|
|
294
|
+
const { result, timeMs } = measureTime(() => {
|
|
295
|
+
return db.getRecentObservations(50);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(timeMs).toBeLessThan(100);
|
|
299
|
+
expect(result.length).toBe(50);
|
|
300
|
+
console.log(`getRecentObservations (10k): ${timeMs.toFixed(2)}ms`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('getRecentObservations with type filter at 10k scale completes in <100ms', () => {
|
|
304
|
+
const { result, timeMs } = measureTime(() => {
|
|
305
|
+
return db.getRecentObservations(50, ['bugfix', 'feature']);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(timeMs).toBeLessThan(100);
|
|
309
|
+
console.log(`getRecentObservations filtered by type (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('getRecentObservations with project filter at 10k scale completes in <100ms', () => {
|
|
313
|
+
const { result, timeMs } = measureTime(() => {
|
|
314
|
+
return db.getRecentObservations(50, undefined, 'project-1');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(timeMs).toBeLessThan(100);
|
|
318
|
+
console.log(`getRecentObservations filtered by project (10k): ${timeMs.toFixed(2)}ms, found ${result.length} results`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('getObservation at 10k scale completes in <100ms', () => {
|
|
322
|
+
const { result, timeMs } = measureTime(() => {
|
|
323
|
+
return db.getObservation(5000);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(timeMs).toBeLessThan(100);
|
|
327
|
+
expect(result).not.toBeNull();
|
|
328
|
+
console.log(`getObservation (10k): ${timeMs.toFixed(2)}ms`);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('createObservation at 10k scale completes in <100ms', () => {
|
|
332
|
+
const { timeMs } = measureTime(() => {
|
|
333
|
+
db.createObservation({
|
|
334
|
+
timestamp: new Date().toISOString(),
|
|
335
|
+
type: 'discovery',
|
|
336
|
+
title: 'New observation at scale',
|
|
337
|
+
content: 'Content after 10k observations exist',
|
|
338
|
+
project: 'test-project',
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(timeMs).toBeLessThan(100);
|
|
343
|
+
console.log(`createObservation (10k): ${timeMs.toFixed(2)}ms`);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('integrityCheck at 10k scale completes in reasonable time', () => {
|
|
347
|
+
const { result, timeMs } = measureTime(() => {
|
|
348
|
+
return db.integrityCheck();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(result.ok).toBe(true);
|
|
352
|
+
// Integrity check can take longer, allow up to 1 second
|
|
353
|
+
expect(timeMs).toBeLessThan(1000);
|
|
354
|
+
console.log(`integrityCheck (10k): ${timeMs.toFixed(2)}ms`);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('FTS5 Search Quality', () => {
|
|
359
|
+
let db: AnvilMemoryDb;
|
|
360
|
+
let testDbPath: string;
|
|
361
|
+
|
|
362
|
+
beforeAll(() => {
|
|
363
|
+
ensureTestDir();
|
|
364
|
+
testDbPath = getTestDbPath('perf-fts');
|
|
365
|
+
db = new AnvilMemoryDb(testDbPath);
|
|
366
|
+
db.init();
|
|
367
|
+
|
|
368
|
+
// Add specific observations for search quality testing
|
|
369
|
+
db.createObservation({
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
type: 'discovery',
|
|
372
|
+
title: 'SQLite FTS5 full-text search implementation',
|
|
373
|
+
content: 'Implemented full-text search using FTS5 with Porter stemmer',
|
|
374
|
+
concepts: ['database', 'search', 'fts5'],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
db.createObservation({
|
|
378
|
+
timestamp: new Date().toISOString(),
|
|
379
|
+
type: 'bugfix',
|
|
380
|
+
title: 'Fixed database connection leak',
|
|
381
|
+
content: 'Resolved issue where database connections were not properly closed',
|
|
382
|
+
concepts: ['database', 'memory', 'leak'],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
db.createObservation({
|
|
386
|
+
timestamp: new Date().toISOString(),
|
|
387
|
+
type: 'feature',
|
|
388
|
+
title: 'Added user authentication',
|
|
389
|
+
content: 'Implemented JWT-based authentication for API endpoints',
|
|
390
|
+
concepts: ['auth', 'jwt', 'security'],
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
afterAll(() => {
|
|
395
|
+
db.close();
|
|
396
|
+
cleanupDb(testDbPath);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('FTS5 finds exact matches', () => {
|
|
400
|
+
const results = db.searchObservations('FTS5');
|
|
401
|
+
expect(results.length).toBeGreaterThan(0);
|
|
402
|
+
expect(results[0]!.title).toContain('FTS5');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('FTS5 finds stemmed matches', () => {
|
|
406
|
+
// "searching" should match "search" due to Porter stemmer
|
|
407
|
+
const results = db.searchObservations('searching');
|
|
408
|
+
expect(results.length).toBeGreaterThan(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('FTS5 finds matches in concepts', () => {
|
|
412
|
+
const results = db.searchObservations('jwt');
|
|
413
|
+
expect(results.length).toBeGreaterThan(0);
|
|
414
|
+
expect(results[0]!.concepts).toContain('jwt');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('FTS5 handles multi-word queries', () => {
|
|
418
|
+
const results = db.searchObservations('database connection');
|
|
419
|
+
expect(results.length).toBeGreaterThan(0);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('FTS5 returns results in relevance order', () => {
|
|
423
|
+
const results = db.searchObservations('database');
|
|
424
|
+
// Should find multiple results with "database"
|
|
425
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
426
|
+
});
|
|
427
|
+
});
|