@soleri/core 7.0.0 → 8.0.0
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/dist/agency/agency-manager.d.ts +27 -1
- package/dist/agency/agency-manager.d.ts.map +1 -1
- package/dist/agency/agency-manager.js +180 -9
- package/dist/agency/agency-manager.js.map +1 -1
- package/dist/agency/default-rules.d.ts +7 -0
- package/dist/agency/default-rules.d.ts.map +1 -0
- package/dist/agency/default-rules.js +79 -0
- package/dist/agency/default-rules.js.map +1 -0
- package/dist/agency/types.d.ts +48 -0
- package/dist/agency/types.d.ts.map +1 -1
- package/dist/brain/brain.d.ts +17 -2
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +118 -8
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/knowledge-synthesizer.d.ts +37 -0
- package/dist/brain/knowledge-synthesizer.d.ts.map +1 -0
- package/dist/brain/knowledge-synthesizer.js +161 -0
- package/dist/brain/knowledge-synthesizer.js.map +1 -0
- package/dist/brain/learning-radar.d.ts +96 -0
- package/dist/brain/learning-radar.d.ts.map +1 -0
- package/dist/brain/learning-radar.js +202 -0
- package/dist/brain/learning-radar.js.map +1 -0
- package/dist/brain/types.d.ts +15 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/context/context-engine.d.ts.map +1 -1
- package/dist/context/context-engine.js +82 -17
- package/dist/context/context-engine.js.map +1 -1
- package/dist/context/types.d.ts +5 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/control/intent-router.d.ts +12 -1
- package/dist/control/intent-router.d.ts.map +1 -1
- package/dist/control/intent-router.js +68 -0
- package/dist/control/intent-router.js.map +1 -1
- package/dist/control/types.d.ts +17 -0
- package/dist/control/types.d.ts.map +1 -1
- package/dist/curator/classifier.d.ts +18 -0
- package/dist/curator/classifier.d.ts.map +1 -0
- package/dist/curator/classifier.js +61 -0
- package/dist/curator/classifier.js.map +1 -0
- package/dist/curator/quality-gate.d.ts +29 -0
- package/dist/curator/quality-gate.d.ts.map +1 -0
- package/dist/curator/quality-gate.js +88 -0
- package/dist/curator/quality-gate.js.map +1 -0
- package/dist/engine/bin/soleri-engine.js +1 -0
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/events/event-bus.d.ts +30 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +51 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/flows/chain-runner.d.ts +46 -0
- package/dist/flows/chain-runner.d.ts.map +1 -0
- package/dist/flows/chain-runner.js +271 -0
- package/dist/flows/chain-runner.js.map +1 -0
- package/dist/flows/chain-types.d.ts +103 -0
- package/dist/flows/chain-types.d.ts.map +1 -0
- package/dist/flows/chain-types.js +23 -0
- package/dist/flows/chain-types.js.map +1 -0
- package/dist/health/doctor-checks.d.ts +15 -0
- package/dist/health/doctor-checks.d.ts.map +1 -0
- package/dist/health/doctor-checks.js +98 -0
- package/dist/health/doctor-checks.js.map +1 -0
- package/dist/intake/text-ingester.d.ts +52 -0
- package/dist/intake/text-ingester.d.ts.map +1 -0
- package/dist/intake/text-ingester.js +181 -0
- package/dist/intake/text-ingester.js.map +1 -0
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +37 -1
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/llm/oauth-discovery.d.ts +26 -0
- package/dist/llm/oauth-discovery.d.ts.map +1 -0
- package/dist/llm/oauth-discovery.js +149 -0
- package/dist/llm/oauth-discovery.js.map +1 -0
- package/dist/planning/evidence-collector.d.ts +41 -0
- package/dist/planning/evidence-collector.d.ts.map +1 -0
- package/dist/planning/evidence-collector.js +194 -0
- package/dist/planning/evidence-collector.js.map +1 -0
- package/dist/planning/planner.d.ts +4 -0
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +11 -0
- package/dist/planning/planner.js.map +1 -1
- package/dist/queue/job-queue.d.ts +92 -0
- package/dist/queue/job-queue.d.ts.map +1 -0
- package/dist/queue/job-queue.js +180 -0
- package/dist/queue/job-queue.js.map +1 -0
- package/dist/queue/pipeline-runner.d.ts +62 -0
- package/dist/queue/pipeline-runner.d.ts.map +1 -0
- package/dist/queue/pipeline-runner.js +126 -0
- package/dist/queue/pipeline-runner.js.map +1 -0
- package/dist/runtime/admin-setup-ops.d.ts +20 -0
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -0
- package/dist/runtime/admin-setup-ops.js +583 -0
- package/dist/runtime/admin-setup-ops.js.map +1 -0
- package/dist/runtime/chain-ops.d.ts +9 -0
- package/dist/runtime/chain-ops.d.ts.map +1 -0
- package/dist/runtime/chain-ops.js +107 -0
- package/dist/runtime/chain-ops.js.map +1 -0
- package/dist/runtime/claude-md-helpers.d.ts +65 -0
- package/dist/runtime/claude-md-helpers.d.ts.map +1 -0
- package/dist/runtime/claude-md-helpers.js +173 -0
- package/dist/runtime/claude-md-helpers.js.map +1 -0
- package/dist/runtime/curator-extra-ops.d.ts +3 -2
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
- package/dist/runtime/curator-extra-ops.js +81 -3
- package/dist/runtime/curator-extra-ops.js.map +1 -1
- package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
- package/dist/runtime/facades/admin-facade.js +4 -0
- package/dist/runtime/facades/admin-facade.js.map +1 -1
- package/dist/runtime/facades/agency-facade.d.ts.map +1 -1
- package/dist/runtime/facades/agency-facade.js +64 -0
- package/dist/runtime/facades/agency-facade.js.map +1 -1
- package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
- package/dist/runtime/facades/brain-facade.js +122 -1
- package/dist/runtime/facades/brain-facade.js.map +1 -1
- package/dist/runtime/facades/control-facade.d.ts.map +1 -1
- package/dist/runtime/facades/control-facade.js +42 -0
- package/dist/runtime/facades/control-facade.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +20 -2
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +2 -0
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +25 -5
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts +7 -5
- package/dist/runtime/intake-ops.d.ts.map +1 -1
- package/dist/runtime/intake-ops.js +98 -5
- package/dist/runtime/intake-ops.js.map +1 -1
- package/dist/runtime/memory-extra-ops.d.ts +6 -3
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -1
- package/dist/runtime/memory-extra-ops.js +292 -4
- package/dist/runtime/memory-extra-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +85 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.js +1 -1
- package/dist/runtime/playbook-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +143 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts +23 -0
- package/dist/runtime/session-briefing.d.ts.map +1 -0
- package/dist/runtime/session-briefing.js +140 -0
- package/dist/runtime/session-briefing.js.map +1 -0
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
- package/dist/runtime/vault-linking-ops.js +1 -3
- package/dist/runtime/vault-linking-ops.js.map +1 -1
- package/dist/vault/vault.d.ts +25 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +67 -3
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/admin-setup-ops.test.ts +355 -0
- package/src/__tests__/async-infrastructure.test.ts +307 -0
- package/src/__tests__/cognee-client-gaps.test.ts +6 -2
- package/src/__tests__/cognee-hybrid-search.test.ts +49 -35
- package/src/__tests__/cognee-sync-manager-deep.test.ts +89 -65
- package/src/__tests__/curator-extra-ops.test.ts +6 -2
- package/src/__tests__/curator-pipeline-e2e.test.ts +358 -0
- package/src/__tests__/memory-extra-ops.test.ts +2 -2
- package/src/__tests__/planning-extra-ops.test.ts +2 -2
- package/src/__tests__/second-brain-features.test.ts +583 -0
- package/src/agency/agency-manager.ts +217 -9
- package/src/agency/default-rules.ts +83 -0
- package/src/agency/types.ts +61 -0
- package/src/brain/brain.ts +110 -8
- package/src/brain/knowledge-synthesizer.ts +218 -0
- package/src/brain/learning-radar.ts +340 -0
- package/src/brain/types.ts +16 -0
- package/src/context/context-engine.ts +114 -15
- package/src/context/types.ts +5 -0
- package/src/control/intent-router.ts +107 -0
- package/src/control/types.ts +10 -0
- package/src/curator/classifier.ts +88 -0
- package/src/curator/quality-gate.ts +129 -0
- package/src/engine/bin/soleri-engine.ts +1 -0
- package/src/events/event-bus.ts +58 -0
- package/src/flows/chain-runner.ts +369 -0
- package/src/flows/chain-types.ts +57 -0
- package/src/health/doctor-checks.ts +115 -0
- package/src/intake/text-ingester.ts +234 -0
- package/src/llm/llm-client.ts +38 -1
- package/src/llm/oauth-discovery.ts +169 -0
- package/src/planning/evidence-collector.ts +247 -0
- package/src/planning/planner.ts +11 -0
- package/src/queue/job-queue.ts +281 -0
- package/src/queue/pipeline-runner.ts +149 -0
- package/src/runtime/admin-setup-ops.ts +664 -0
- package/src/runtime/chain-ops.ts +121 -0
- package/src/runtime/claude-md-helpers.ts +236 -0
- package/src/runtime/curator-extra-ops.ts +86 -3
- package/src/runtime/facades/admin-facade.ts +4 -0
- package/src/runtime/facades/agency-facade.ts +68 -0
- package/src/runtime/facades/brain-facade.ts +142 -1
- package/src/runtime/facades/control-facade.ts +45 -0
- package/src/runtime/facades/memory-facade.ts +20 -2
- package/src/runtime/facades/plan-facade.ts +2 -0
- package/src/runtime/facades/vault-facade.ts +28 -5
- package/src/runtime/intake-ops.ts +107 -5
- package/src/runtime/memory-extra-ops.ts +312 -4
- package/src/runtime/planning-extra-ops.ts +94 -0
- package/src/runtime/playbook-ops.ts +1 -1
- package/src/runtime/runtime.ts +138 -2
- package/src/runtime/session-briefing.ts +161 -0
- package/src/runtime/types.ts +23 -0
- package/src/runtime/vault-linking-ops.ts +1 -3
- package/src/vault/vault.ts +79 -4
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { createAgentRuntime } from '../runtime/runtime.js';
|
|
6
|
+
import { createAdminSetupOps } from '../runtime/admin-setup-ops.js';
|
|
7
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
8
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
9
|
+
import {
|
|
10
|
+
hasSections,
|
|
11
|
+
removeSections,
|
|
12
|
+
injectAtPosition,
|
|
13
|
+
buildInjectionContent,
|
|
14
|
+
wrapInMarkers,
|
|
15
|
+
composeAgentModeSection,
|
|
16
|
+
composeIntegrationSection,
|
|
17
|
+
} from '../runtime/claude-md-helpers.js';
|
|
18
|
+
|
|
19
|
+
// ─── claude-md-helpers tests ──────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe('claude-md-helpers', () => {
|
|
22
|
+
const agentId = 'test-agent';
|
|
23
|
+
const config = { agentId } as any;
|
|
24
|
+
|
|
25
|
+
describe('hasSections', () => {
|
|
26
|
+
it('returns true when markers present', () => {
|
|
27
|
+
const content = `# Hello\n<!-- agent:${agentId}:mode -->\nstuff\n<!-- /agent:${agentId}:mode -->\n`;
|
|
28
|
+
expect(hasSections(content, agentId)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false when no markers', () => {
|
|
32
|
+
expect(hasSections('# Hello\nWorld', agentId)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns false for different agent', () => {
|
|
36
|
+
const content = `<!-- agent:other:mode -->\nstuff\n<!-- /agent:other:mode -->`;
|
|
37
|
+
expect(hasSections(content, agentId)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('removeSections', () => {
|
|
42
|
+
it('removes agent section cleanly', () => {
|
|
43
|
+
const content = `# Hello\n\n<!-- agent:${agentId}:mode -->\nstuff\n<!-- /agent:${agentId}:mode -->\n\n# Footer`;
|
|
44
|
+
const result = removeSections(content, agentId);
|
|
45
|
+
expect(result).not.toContain('stuff');
|
|
46
|
+
expect(result).toContain('# Hello');
|
|
47
|
+
expect(result).toContain('# Footer');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns unchanged content if no markers', () => {
|
|
51
|
+
const content = '# Hello\nWorld';
|
|
52
|
+
expect(removeSections(content, agentId)).toBe(content);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('injectAtPosition', () => {
|
|
57
|
+
const section = '## Injected';
|
|
58
|
+
|
|
59
|
+
it('injects at start', () => {
|
|
60
|
+
const result = injectAtPosition('# Hello', section, 'start');
|
|
61
|
+
expect(result.startsWith('## Injected')).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('injects at end', () => {
|
|
65
|
+
const result = injectAtPosition('# Hello', section, 'end');
|
|
66
|
+
expect(result.endsWith('## Injected\n')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('injects after title', () => {
|
|
70
|
+
const content = '# My Project\n\nSome content here.';
|
|
71
|
+
const result = injectAtPosition(content, section, 'after-title');
|
|
72
|
+
const lines = result.split('\n');
|
|
73
|
+
const titleIdx = lines.indexOf('# My Project');
|
|
74
|
+
expect(titleIdx).toBe(0);
|
|
75
|
+
// Section should appear before "Some content here."
|
|
76
|
+
expect(result.indexOf('## Injected')).toBeLessThan(result.indexOf('Some content here.'));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to start if no title found', () => {
|
|
80
|
+
const content = 'No heading here.';
|
|
81
|
+
const result = injectAtPosition(content, section, 'after-title');
|
|
82
|
+
expect(result.startsWith('## Injected')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('wrapInMarkers', () => {
|
|
87
|
+
it('wraps content in agent markers', () => {
|
|
88
|
+
const result = wrapInMarkers(agentId, 'hello');
|
|
89
|
+
expect(result).toContain(`<!-- agent:${agentId}:mode -->`);
|
|
90
|
+
expect(result).toContain('hello');
|
|
91
|
+
expect(result).toContain(`<!-- /agent:${agentId}:mode -->`);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('composeAgentModeSection', () => {
|
|
96
|
+
it('generates activation commands', () => {
|
|
97
|
+
const result = composeAgentModeSection(config);
|
|
98
|
+
expect(result).toContain('## Test-agent Mode');
|
|
99
|
+
expect(result).toContain('test-agent_core op:activate');
|
|
100
|
+
expect(result).toContain('deactivate: true');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('composeIntegrationSection', () => {
|
|
105
|
+
it('generates default tools table when no facades provided', () => {
|
|
106
|
+
const result = composeIntegrationSection(config);
|
|
107
|
+
expect(result).toContain('## Test-agent Integration');
|
|
108
|
+
expect(result).toContain('test-agent_vault');
|
|
109
|
+
expect(result).toContain('test-agent_admin');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses provided facades', () => {
|
|
113
|
+
const facades = [{ name: 'test-agent_custom', ops: ['op_a', 'op_b'] }];
|
|
114
|
+
const result = composeIntegrationSection(config, facades);
|
|
115
|
+
expect(result).toContain('test-agent_custom');
|
|
116
|
+
expect(result).toContain('`op_a`');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('buildInjectionContent', () => {
|
|
121
|
+
it('wraps in markers', () => {
|
|
122
|
+
const result = buildInjectionContent(config);
|
|
123
|
+
expect(result).toContain(`<!-- agent:${agentId}:mode -->`);
|
|
124
|
+
expect(result).toContain(`<!-- /agent:${agentId}:mode -->`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('excludes integration when disabled', () => {
|
|
128
|
+
const result = buildInjectionContent(config, { includeIntegration: false });
|
|
129
|
+
expect(result).toContain('Mode');
|
|
130
|
+
expect(result).not.toContain('Integration');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ─── admin-setup-ops tests ────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe('createAdminSetupOps', () => {
|
|
138
|
+
let runtime: AgentRuntime;
|
|
139
|
+
let ops: OpDefinition[];
|
|
140
|
+
let tmpDir: string;
|
|
141
|
+
|
|
142
|
+
function findOp(name: string): OpDefinition {
|
|
143
|
+
const op = ops.find((o) => o.name === name);
|
|
144
|
+
if (!op) throw new Error(`Op "${name}" not found`);
|
|
145
|
+
return op;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
tmpDir = join(tmpdir(), `soleri-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
150
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
runtime = createAgentRuntime({
|
|
153
|
+
agentId: 'test-setup',
|
|
154
|
+
vaultPath: ':memory:',
|
|
155
|
+
});
|
|
156
|
+
ops = createAdminSetupOps(runtime);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
runtime?.close();
|
|
161
|
+
try {
|
|
162
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
163
|
+
} catch {
|
|
164
|
+
// cleanup best-effort
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return 4 ops', () => {
|
|
169
|
+
expect(ops).toHaveLength(4);
|
|
170
|
+
const names = ops.map((o) => o.name);
|
|
171
|
+
expect(names).toEqual([
|
|
172
|
+
'admin_inject_claude_md',
|
|
173
|
+
'admin_setup_global',
|
|
174
|
+
'admin_setup_project',
|
|
175
|
+
'admin_check_persistence',
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ─── inject_claude_md ───────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
describe('admin_inject_claude_md', () => {
|
|
182
|
+
it('creates CLAUDE.md when createIfMissing is true', async () => {
|
|
183
|
+
const result = (await findOp('admin_inject_claude_md').handler({
|
|
184
|
+
projectPath: tmpDir,
|
|
185
|
+
createIfMissing: true,
|
|
186
|
+
includeIntegration: true,
|
|
187
|
+
position: 'after-title',
|
|
188
|
+
dryRun: false,
|
|
189
|
+
global: false,
|
|
190
|
+
})) as any;
|
|
191
|
+
|
|
192
|
+
expect(result.action).toBe('created');
|
|
193
|
+
expect(existsSync(result.path)).toBe(true);
|
|
194
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
195
|
+
expect(content).toContain('<!-- agent:test-setup:mode -->');
|
|
196
|
+
expect(content).toContain('test-setup_core op:activate');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('errors when CLAUDE.md not found and createIfMissing is false', async () => {
|
|
200
|
+
const result = (await findOp('admin_inject_claude_md').handler({
|
|
201
|
+
projectPath: tmpDir,
|
|
202
|
+
createIfMissing: false,
|
|
203
|
+
includeIntegration: true,
|
|
204
|
+
position: 'after-title',
|
|
205
|
+
dryRun: false,
|
|
206
|
+
global: false,
|
|
207
|
+
})) as any;
|
|
208
|
+
|
|
209
|
+
expect(result.action).toBe('error');
|
|
210
|
+
expect(result.error).toContain('not found');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('injects into existing CLAUDE.md', async () => {
|
|
214
|
+
writeFileSync(join(tmpDir, 'CLAUDE.md'), '# My Project\n\nExisting content.\n');
|
|
215
|
+
|
|
216
|
+
const result = (await findOp('admin_inject_claude_md').handler({
|
|
217
|
+
projectPath: tmpDir,
|
|
218
|
+
createIfMissing: false,
|
|
219
|
+
includeIntegration: true,
|
|
220
|
+
position: 'after-title',
|
|
221
|
+
dryRun: false,
|
|
222
|
+
global: false,
|
|
223
|
+
})) as any;
|
|
224
|
+
|
|
225
|
+
expect(result.action).toBe('injected');
|
|
226
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
227
|
+
expect(content).toContain('# My Project');
|
|
228
|
+
expect(content).toContain('<!-- agent:test-setup:mode -->');
|
|
229
|
+
expect(content).toContain('Existing content.');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('is idempotent — updates existing sections', async () => {
|
|
233
|
+
writeFileSync(join(tmpDir, 'CLAUDE.md'), '# My Project\n\nExisting content.\n');
|
|
234
|
+
|
|
235
|
+
// First injection
|
|
236
|
+
await findOp('admin_inject_claude_md').handler({
|
|
237
|
+
projectPath: tmpDir,
|
|
238
|
+
createIfMissing: false,
|
|
239
|
+
includeIntegration: true,
|
|
240
|
+
position: 'after-title',
|
|
241
|
+
dryRun: false,
|
|
242
|
+
global: false,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Second injection — should update, not duplicate
|
|
246
|
+
const result = (await findOp('admin_inject_claude_md').handler({
|
|
247
|
+
projectPath: tmpDir,
|
|
248
|
+
createIfMissing: false,
|
|
249
|
+
includeIntegration: true,
|
|
250
|
+
position: 'after-title',
|
|
251
|
+
dryRun: false,
|
|
252
|
+
global: false,
|
|
253
|
+
})) as any;
|
|
254
|
+
|
|
255
|
+
expect(result.action).toBe('updated');
|
|
256
|
+
const content = readFileSync(result.path, 'utf-8');
|
|
257
|
+
// Should have exactly one pair of markers
|
|
258
|
+
const markerCount = (content.match(/<!-- agent:test-setup:mode -->/g) ?? []).length;
|
|
259
|
+
expect(markerCount).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('supports dry-run mode', async () => {
|
|
263
|
+
writeFileSync(join(tmpDir, 'CLAUDE.md'), '# My Project\n');
|
|
264
|
+
|
|
265
|
+
const result = (await findOp('admin_inject_claude_md').handler({
|
|
266
|
+
projectPath: tmpDir,
|
|
267
|
+
createIfMissing: false,
|
|
268
|
+
includeIntegration: true,
|
|
269
|
+
position: 'after-title',
|
|
270
|
+
dryRun: true,
|
|
271
|
+
global: false,
|
|
272
|
+
})) as any;
|
|
273
|
+
|
|
274
|
+
expect(result.action).toBe('would_inject');
|
|
275
|
+
expect(result.preview).toBeDefined();
|
|
276
|
+
// File should not be modified
|
|
277
|
+
const content = readFileSync(join(tmpDir, 'CLAUDE.md'), 'utf-8');
|
|
278
|
+
expect(content).not.toContain('agent:test-setup:mode');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── setup_global ───────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('admin_setup_global', () => {
|
|
285
|
+
it('returns dry-run analysis when install is false', async () => {
|
|
286
|
+
const result = (await findOp('admin_setup_global').handler({
|
|
287
|
+
install: false,
|
|
288
|
+
hooksOnly: false,
|
|
289
|
+
settingsJsonOnly: false,
|
|
290
|
+
skillsOnly: false,
|
|
291
|
+
})) as any;
|
|
292
|
+
|
|
293
|
+
expect(result.dryRun).toBe(true);
|
|
294
|
+
expect(result.agentId).toBe('test-setup');
|
|
295
|
+
expect(result.hookifyRules).toBeDefined();
|
|
296
|
+
expect(result.skills).toBeDefined();
|
|
297
|
+
expect(result.settingsJson).toBeDefined();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ─── setup_project ─────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
describe('admin_setup_project', () => {
|
|
304
|
+
it('returns analysis in default mode', async () => {
|
|
305
|
+
const result = (await findOp('admin_setup_project').handler({
|
|
306
|
+
projectPath: tmpDir,
|
|
307
|
+
cleanup: false,
|
|
308
|
+
install: false,
|
|
309
|
+
})) as any;
|
|
310
|
+
|
|
311
|
+
expect(result.mode).toBe('analyze');
|
|
312
|
+
expect(result.projectPath).toBe(tmpDir);
|
|
313
|
+
expect(typeof result.globalHooks).toBe('number');
|
|
314
|
+
expect(typeof result.projectHooks).toBe('number');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('returns error for non-existent project', async () => {
|
|
318
|
+
const result = (await findOp('admin_setup_project').handler({
|
|
319
|
+
projectPath: '/nonexistent/path',
|
|
320
|
+
cleanup: false,
|
|
321
|
+
install: false,
|
|
322
|
+
})) as any;
|
|
323
|
+
|
|
324
|
+
expect(result.error).toBe('PROJECT_NOT_FOUND');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ─── check_persistence ──────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe('admin_check_persistence', () => {
|
|
331
|
+
it('returns persistence diagnostic', async () => {
|
|
332
|
+
const result = (await findOp('admin_check_persistence').handler({})) as any;
|
|
333
|
+
|
|
334
|
+
expect(result.agentId).toBe('test-setup');
|
|
335
|
+
expect(result.storageDirectory).toBeDefined();
|
|
336
|
+
expect(result.files).toBeDefined();
|
|
337
|
+
expect(result.files.plans).toBeDefined();
|
|
338
|
+
expect(result.files.vault).toBeDefined();
|
|
339
|
+
expect(result.status).toBeDefined();
|
|
340
|
+
expect(result.recommendation).toBeDefined();
|
|
341
|
+
expect(Array.isArray(result.activePlans)).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('reports correct status for in-memory vault', async () => {
|
|
345
|
+
const result = (await findOp('admin_check_persistence').handler({})) as any;
|
|
346
|
+
|
|
347
|
+
// In-memory vault + no plans file = NO_STORAGE_DIRECTORY or PERSISTENCE_CONFIGURED_BUT_INCOMPLETE
|
|
348
|
+
expect([
|
|
349
|
+
'NO_STORAGE_DIRECTORY',
|
|
350
|
+
'PERSISTENCE_CONFIGURED_BUT_INCOMPLETE',
|
|
351
|
+
'PERSISTENCE_ACTIVE',
|
|
352
|
+
]).toContain(result.status);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for async infrastructure — event bus, job queue, pipeline runner.
|
|
3
|
+
* Phase 1 of #210: generic infrastructure modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
7
|
+
import { TypedEventBus } from '../events/event-bus.js';
|
|
8
|
+
import { JobQueue } from '../queue/job-queue.js';
|
|
9
|
+
import { PipelineRunner } from '../queue/pipeline-runner.js';
|
|
10
|
+
import { Vault } from '../vault/vault.js';
|
|
11
|
+
|
|
12
|
+
// ─── Event Bus ───────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe('TypedEventBus', () => {
|
|
15
|
+
type TestEvents = {
|
|
16
|
+
'item:created': { id: string; title: string };
|
|
17
|
+
'item:deleted': { id: string };
|
|
18
|
+
tick: { count: number };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
it('emits and receives typed events', () => {
|
|
22
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
23
|
+
let received: TestEvents['item:created'] | null = null;
|
|
24
|
+
|
|
25
|
+
bus.on('item:created', (payload) => {
|
|
26
|
+
received = payload;
|
|
27
|
+
});
|
|
28
|
+
bus.emit('item:created', { id: '1', title: 'Hello' });
|
|
29
|
+
|
|
30
|
+
expect(received).toEqual({ id: '1', title: 'Hello' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('once listener fires only once', () => {
|
|
34
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
35
|
+
let count = 0;
|
|
36
|
+
|
|
37
|
+
bus.once('tick', () => {
|
|
38
|
+
count++;
|
|
39
|
+
});
|
|
40
|
+
bus.emit('tick', { count: 1 });
|
|
41
|
+
bus.emit('tick', { count: 2 });
|
|
42
|
+
|
|
43
|
+
expect(count).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('off removes a listener', () => {
|
|
47
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
48
|
+
let count = 0;
|
|
49
|
+
const listener = () => {
|
|
50
|
+
count++;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
bus.on('tick', listener);
|
|
54
|
+
bus.emit('tick', { count: 1 });
|
|
55
|
+
bus.off('tick', listener);
|
|
56
|
+
bus.emit('tick', { count: 2 });
|
|
57
|
+
|
|
58
|
+
expect(count).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('listenerCount tracks total listeners', () => {
|
|
62
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
63
|
+
expect(bus.listenerCount()).toBe(0);
|
|
64
|
+
|
|
65
|
+
bus.on('item:created', () => {});
|
|
66
|
+
bus.on('item:deleted', () => {});
|
|
67
|
+
expect(bus.listenerCount()).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('removeAllListeners clears everything', () => {
|
|
71
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
72
|
+
bus.on('item:created', () => {});
|
|
73
|
+
bus.on('tick', () => {});
|
|
74
|
+
bus.removeAllListeners();
|
|
75
|
+
expect(bus.listenerCount()).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('supports multiple listeners on same event', () => {
|
|
79
|
+
const bus = new TypedEventBus<TestEvents>();
|
|
80
|
+
const results: string[] = [];
|
|
81
|
+
|
|
82
|
+
bus.on('item:created', (p) => results.push('A:' + p.id));
|
|
83
|
+
bus.on('item:created', (p) => results.push('B:' + p.id));
|
|
84
|
+
bus.emit('item:created', { id: '1', title: 'Test' });
|
|
85
|
+
|
|
86
|
+
expect(results).toEqual(['A:1', 'B:1']);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── Job Queue ───────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('JobQueue', () => {
|
|
93
|
+
let vault: Vault;
|
|
94
|
+
let queue: JobQueue;
|
|
95
|
+
|
|
96
|
+
beforeAll(() => {
|
|
97
|
+
vault = new Vault(':memory:');
|
|
98
|
+
queue = new JobQueue(vault.getProvider());
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterAll(() => {
|
|
102
|
+
vault.close();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('enqueue creates a job and returns ID', () => {
|
|
106
|
+
const id = queue.enqueue('tag-normalize', { entryId: 'e1' });
|
|
107
|
+
expect(id).toBeTruthy();
|
|
108
|
+
expect(id.length).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('dequeue returns oldest pending job', () => {
|
|
112
|
+
const id = queue.enqueue('dedup-check');
|
|
113
|
+
const job = queue.dequeue();
|
|
114
|
+
expect(job).not.toBeNull();
|
|
115
|
+
expect(job!.type).toBe('tag-normalize'); // First enqueued from previous test
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('dequeue marks job as running', () => {
|
|
119
|
+
const job = queue.get(queue.enqueue('test-job'));
|
|
120
|
+
expect(job!.status).toBe('pending');
|
|
121
|
+
|
|
122
|
+
const dequeued = queue.dequeue();
|
|
123
|
+
expect(dequeued!.status).toBe('running');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('complete marks job as completed with result', () => {
|
|
127
|
+
const id = queue.enqueue('test-complete');
|
|
128
|
+
queue.dequeue(); // Mark running
|
|
129
|
+
queue.complete(id, { ok: true });
|
|
130
|
+
|
|
131
|
+
const job = queue.get(id);
|
|
132
|
+
expect(job!.status).toBe('completed');
|
|
133
|
+
expect(job!.result).toEqual({ ok: true });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('fail marks job as failed with error', () => {
|
|
137
|
+
const id = queue.enqueue('test-fail');
|
|
138
|
+
queue.dequeue();
|
|
139
|
+
queue.fail(id, 'something went wrong');
|
|
140
|
+
|
|
141
|
+
const job = queue.get(id);
|
|
142
|
+
expect(job!.status).toBe('failed');
|
|
143
|
+
expect(job!.error).toBe('something went wrong');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('retry resets failed job to pending', () => {
|
|
147
|
+
const id = queue.enqueue('test-retry');
|
|
148
|
+
queue.dequeue();
|
|
149
|
+
queue.fail(id, 'transient error');
|
|
150
|
+
|
|
151
|
+
const retried = queue.retry(id);
|
|
152
|
+
expect(retried).toBe(true);
|
|
153
|
+
|
|
154
|
+
const job = queue.get(id);
|
|
155
|
+
expect(job!.status).toBe('pending');
|
|
156
|
+
expect(job!.retryCount).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('retry returns false when max retries exceeded', () => {
|
|
160
|
+
const id = queue.enqueue('test-max-retry', { maxRetries: 1 });
|
|
161
|
+
queue.dequeue();
|
|
162
|
+
queue.fail(id, 'fail 1');
|
|
163
|
+
queue.retry(id);
|
|
164
|
+
queue.dequeue();
|
|
165
|
+
queue.fail(id, 'fail 2');
|
|
166
|
+
|
|
167
|
+
const retried = queue.retry(id);
|
|
168
|
+
expect(retried).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('respects DAG dependencies', () => {
|
|
172
|
+
const dep1 = queue.enqueue('dep-job-1');
|
|
173
|
+
const dep2 = queue.enqueue('dep-job-2', { dependsOn: [dep1] });
|
|
174
|
+
|
|
175
|
+
// dep2 should not dequeue because dep1 is not completed
|
|
176
|
+
// Clear running jobs first
|
|
177
|
+
const ready = queue.dequeueReady(10);
|
|
178
|
+
const readyIds = ready.map((j) => j.id);
|
|
179
|
+
expect(readyIds).toContain(dep1);
|
|
180
|
+
expect(readyIds).not.toContain(dep2);
|
|
181
|
+
|
|
182
|
+
// Complete dep1
|
|
183
|
+
queue.complete(dep1, {});
|
|
184
|
+
|
|
185
|
+
// Now dep2 should be ready
|
|
186
|
+
const ready2 = queue.dequeueReady(10);
|
|
187
|
+
const readyIds2 = ready2.map((j) => j.id);
|
|
188
|
+
expect(readyIds2).toContain(dep2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('groups jobs by pipeline', () => {
|
|
192
|
+
const pid = 'pipeline-123';
|
|
193
|
+
queue.enqueue('step-1', { pipelineId: pid });
|
|
194
|
+
queue.enqueue('step-2', { pipelineId: pid });
|
|
195
|
+
|
|
196
|
+
const jobs = queue.getByPipeline(pid);
|
|
197
|
+
expect(jobs.length).toBe(2);
|
|
198
|
+
expect(jobs.every((j) => j.pipelineId === pid)).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('getStats returns correct counts', () => {
|
|
202
|
+
const stats = queue.getStats();
|
|
203
|
+
expect(stats.total).toBeGreaterThan(0);
|
|
204
|
+
expect(typeof stats.pending).toBe('number');
|
|
205
|
+
expect(typeof stats.running).toBe('number');
|
|
206
|
+
expect(typeof stats.completed).toBe('number');
|
|
207
|
+
expect(typeof stats.failed).toBe('number');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('purge removes old completed/failed jobs', () => {
|
|
211
|
+
// purge with 0 days should remove all completed/failed
|
|
212
|
+
const purged = queue.purge(0);
|
|
213
|
+
expect(purged).toBeGreaterThanOrEqual(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── Pipeline Runner ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('PipelineRunner', () => {
|
|
220
|
+
let vault: Vault;
|
|
221
|
+
let queue: JobQueue;
|
|
222
|
+
let runner: PipelineRunner;
|
|
223
|
+
|
|
224
|
+
beforeAll(() => {
|
|
225
|
+
vault = new Vault(':memory:');
|
|
226
|
+
queue = new JobQueue(vault.getProvider());
|
|
227
|
+
runner = new PipelineRunner(queue, 100); // 100ms poll for tests
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
afterAll(() => {
|
|
231
|
+
runner.stop();
|
|
232
|
+
vault.close();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('registerHandler stores a handler for a job type', () => {
|
|
236
|
+
runner.registerHandler('test-type', async () => ({ done: true }));
|
|
237
|
+
expect(runner.hasHandler('test-type')).toBe(true);
|
|
238
|
+
expect(runner.hasHandler('unknown-type')).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('processOnce dequeues and completes jobs', async () => {
|
|
242
|
+
const handler = vi.fn().mockResolvedValue({ processed: true });
|
|
243
|
+
runner.registerHandler('process-test', handler);
|
|
244
|
+
|
|
245
|
+
queue.enqueue('process-test', { payload: { key: 'value' } });
|
|
246
|
+
|
|
247
|
+
const processed = await runner.processOnce();
|
|
248
|
+
expect(processed).toBe(1);
|
|
249
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('processOnce fails jobs with no handler', async () => {
|
|
253
|
+
const id = queue.enqueue('no-handler-type');
|
|
254
|
+
await runner.processOnce();
|
|
255
|
+
|
|
256
|
+
const job = queue.get(id);
|
|
257
|
+
expect(job!.status).toBe('failed');
|
|
258
|
+
expect(job!.error).toContain('No handler registered');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('processOnce retries on handler error', async () => {
|
|
262
|
+
const handler = vi.fn().mockRejectedValue(new Error('transient'));
|
|
263
|
+
runner.registerHandler('flaky-type', handler);
|
|
264
|
+
|
|
265
|
+
const id = queue.enqueue('flaky-type');
|
|
266
|
+
await runner.processOnce();
|
|
267
|
+
|
|
268
|
+
const job = queue.get(id);
|
|
269
|
+
// Should be pending again (retried), not failed
|
|
270
|
+
expect(job!.status).toBe('pending');
|
|
271
|
+
expect(job!.retryCount).toBe(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('getStatus returns runner state', () => {
|
|
275
|
+
const status = runner.getStatus();
|
|
276
|
+
expect(status.running).toBe(false); // Not started yet
|
|
277
|
+
expect(status.pollIntervalMs).toBe(100);
|
|
278
|
+
expect(status.jobsProcessed).toBeGreaterThanOrEqual(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('start/stop controls background polling', async () => {
|
|
282
|
+
runner.start();
|
|
283
|
+
expect(runner.getStatus().running).toBe(true);
|
|
284
|
+
|
|
285
|
+
runner.stop();
|
|
286
|
+
expect(runner.getStatus().running).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('respects DAG in pipeline execution', async () => {
|
|
290
|
+
const order: string[] = [];
|
|
291
|
+
runner.registerHandler('dag-step', async (job) => {
|
|
292
|
+
order.push(job.id);
|
|
293
|
+
return { step: job.id };
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const step1 = queue.enqueue('dag-step', { pipelineId: 'dag-test' });
|
|
297
|
+
const step2 = queue.enqueue('dag-step', { pipelineId: 'dag-test', dependsOn: [step1] });
|
|
298
|
+
|
|
299
|
+
// First batch: only step1 should process
|
|
300
|
+
await runner.processOnce();
|
|
301
|
+
expect(order).toEqual([step1]);
|
|
302
|
+
|
|
303
|
+
// Second batch: step2 should now be ready
|
|
304
|
+
await runner.processOnce();
|
|
305
|
+
expect(order).toEqual([step1, step2]);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -357,7 +357,10 @@ describe('CogneeClient — gap coverage', () => {
|
|
|
357
357
|
mockWithAuth(
|
|
358
358
|
async () =>
|
|
359
359
|
new Response(
|
|
360
|
-
JSON.stringify([
|
|
360
|
+
JSON.stringify([
|
|
361
|
+
{ id: 'r1', text: 42, score: 0.8 },
|
|
362
|
+
{ id: 'r2', score: 0.7 },
|
|
363
|
+
]),
|
|
361
364
|
{ status: 200 },
|
|
362
365
|
),
|
|
363
366
|
);
|
|
@@ -440,7 +443,8 @@ describe('CogneeClient — gap coverage', () => {
|
|
|
440
443
|
|
|
441
444
|
it('should give single result score 1.0', async () => {
|
|
442
445
|
mockWithAuth(
|
|
443
|
-
async () =>
|
|
446
|
+
async () =>
|
|
447
|
+
new Response(JSON.stringify([{ id: 'only', text: 'Sole result' }]), { status: 200 }),
|
|
444
448
|
);
|
|
445
449
|
const client = new CogneeClient();
|
|
446
450
|
await client.healthCheck();
|