@soleri/core 2.0.2 → 2.4.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/brain/brain.d.ts +14 -50
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +207 -16
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +86 -0
- package/dist/brain/intelligence.d.ts.map +1 -0
- package/dist/brain/intelligence.js +771 -0
- package/dist/brain/intelligence.js.map +1 -0
- package/dist/brain/types.d.ts +197 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +2 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/cognee/client.d.ts +35 -0
- package/dist/cognee/client.d.ts.map +1 -0
- package/dist/cognee/client.js +291 -0
- package/dist/cognee/client.js.map +1 -0
- package/dist/cognee/types.d.ts +46 -0
- package/dist/cognee/types.d.ts.map +1 -0
- package/dist/cognee/types.js +3 -0
- package/dist/cognee/types.js.map +1 -0
- package/dist/control/identity-manager.d.ts +22 -0
- package/dist/control/identity-manager.d.ts.map +1 -0
- package/dist/control/identity-manager.js +233 -0
- package/dist/control/identity-manager.js.map +1 -0
- package/dist/control/intent-router.d.ts +32 -0
- package/dist/control/intent-router.d.ts.map +1 -0
- package/dist/control/intent-router.js +242 -0
- package/dist/control/intent-router.js.map +1 -0
- package/dist/control/types.d.ts +68 -0
- package/dist/control/types.d.ts.map +1 -0
- package/dist/control/types.js +9 -0
- package/dist/control/types.js.map +1 -0
- package/dist/curator/curator.d.ts +29 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +142 -5
- package/dist/curator/curator.js.map +1 -1
- package/dist/facades/types.d.ts +1 -1
- package/dist/governance/governance.d.ts +42 -0
- package/dist/governance/governance.d.ts.map +1 -0
- package/dist/governance/governance.js +488 -0
- package/dist/governance/governance.js.map +1 -0
- package/dist/governance/index.d.ts +3 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +2 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/types.d.ts +102 -0
- package/dist/governance/types.d.ts.map +1 -0
- package/dist/governance/types.js +3 -0
- package/dist/governance/types.js.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +9 -2
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/logging/logger.d.ts +37 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +145 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/types.d.ts +19 -0
- package/dist/logging/types.d.ts.map +1 -0
- package/dist/logging/types.js +2 -0
- package/dist/logging/types.js.map +1 -0
- package/dist/loop/loop-manager.d.ts +49 -0
- package/dist/loop/loop-manager.d.ts.map +1 -0
- package/dist/loop/loop-manager.js +105 -0
- package/dist/loop/loop-manager.js.map +1 -0
- package/dist/loop/types.d.ts +35 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +8 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/planning/gap-analysis.d.ts +29 -0
- package/dist/planning/gap-analysis.d.ts.map +1 -0
- package/dist/planning/gap-analysis.js +265 -0
- package/dist/planning/gap-analysis.js.map +1 -0
- package/dist/planning/gap-types.d.ts +29 -0
- package/dist/planning/gap-types.d.ts.map +1 -0
- package/dist/planning/gap-types.js +28 -0
- package/dist/planning/gap-types.js.map +1 -0
- package/dist/planning/planner.d.ts +150 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +365 -2
- package/dist/planning/planner.js.map +1 -1
- package/dist/project/project-registry.d.ts +79 -0
- package/dist/project/project-registry.d.ts.map +1 -0
- package/dist/project/project-registry.js +276 -0
- package/dist/project/project-registry.js.map +1 -0
- package/dist/project/types.d.ts +28 -0
- package/dist/project/types.d.ts.map +1 -0
- package/dist/project/types.js +5 -0
- package/dist/project/types.js.map +1 -0
- package/dist/runtime/admin-extra-ops.d.ts +13 -0
- package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
- package/dist/runtime/admin-extra-ops.js +284 -0
- package/dist/runtime/admin-extra-ops.js.map +1 -0
- package/dist/runtime/admin-ops.d.ts +15 -0
- package/dist/runtime/admin-ops.d.ts.map +1 -0
- package/dist/runtime/admin-ops.js +322 -0
- package/dist/runtime/admin-ops.js.map +1 -0
- package/dist/runtime/capture-ops.d.ts +15 -0
- package/dist/runtime/capture-ops.d.ts.map +1 -0
- package/dist/runtime/capture-ops.js +345 -0
- package/dist/runtime/capture-ops.js.map +1 -0
- package/dist/runtime/core-ops.d.ts +7 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +646 -15
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/curator-extra-ops.d.ts +9 -0
- package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
- package/dist/runtime/curator-extra-ops.js +59 -0
- package/dist/runtime/curator-extra-ops.js.map +1 -0
- package/dist/runtime/domain-ops.d.ts.map +1 -1
- package/dist/runtime/domain-ops.js +59 -13
- package/dist/runtime/domain-ops.js.map +1 -1
- package/dist/runtime/grading-ops.d.ts +14 -0
- package/dist/runtime/grading-ops.d.ts.map +1 -0
- package/dist/runtime/grading-ops.js +105 -0
- package/dist/runtime/grading-ops.js.map +1 -0
- package/dist/runtime/loop-ops.d.ts +13 -0
- package/dist/runtime/loop-ops.d.ts.map +1 -0
- package/dist/runtime/loop-ops.js +179 -0
- package/dist/runtime/loop-ops.js.map +1 -0
- package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
- package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
- package/dist/runtime/memory-cross-project-ops.js +165 -0
- package/dist/runtime/memory-cross-project-ops.js.map +1 -0
- package/dist/runtime/memory-extra-ops.d.ts +13 -0
- package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
- package/dist/runtime/memory-extra-ops.js +173 -0
- package/dist/runtime/memory-extra-ops.js.map +1 -0
- package/dist/runtime/orchestrate-ops.d.ts +17 -0
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
- package/dist/runtime/orchestrate-ops.js +240 -0
- package/dist/runtime/orchestrate-ops.js.map +1 -0
- package/dist/runtime/planning-extra-ops.d.ts +17 -0
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
- package/dist/runtime/planning-extra-ops.js +300 -0
- package/dist/runtime/planning-extra-ops.js.map +1 -0
- package/dist/runtime/project-ops.d.ts +15 -0
- package/dist/runtime/project-ops.d.ts.map +1 -0
- package/dist/runtime/project-ops.js +181 -0
- package/dist/runtime/project-ops.js.map +1 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +48 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +23 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts +9 -0
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
- package/dist/runtime/vault-extra-ops.js +195 -0
- package/dist/runtime/vault-extra-ops.js.map +1 -0
- package/dist/telemetry/telemetry.d.ts +48 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/telemetry/telemetry.js +87 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/vault/vault.d.ts +94 -0
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +340 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/admin-extra-ops.test.ts +420 -0
- package/src/__tests__/admin-ops.test.ts +271 -0
- package/src/__tests__/brain-intelligence.test.ts +828 -0
- package/src/__tests__/brain.test.ts +396 -27
- package/src/__tests__/capture-ops.test.ts +509 -0
- package/src/__tests__/cognee-client.test.ts +524 -0
- package/src/__tests__/core-ops.test.ts +341 -49
- package/src/__tests__/curator-extra-ops.test.ts +359 -0
- package/src/__tests__/curator.test.ts +126 -31
- package/src/__tests__/domain-ops.test.ts +111 -9
- package/src/__tests__/governance.test.ts +522 -0
- package/src/__tests__/grading-ops.test.ts +340 -0
- package/src/__tests__/identity-manager.test.ts +243 -0
- package/src/__tests__/intent-router.test.ts +222 -0
- package/src/__tests__/logger.test.ts +200 -0
- package/src/__tests__/loop-ops.test.ts +398 -0
- package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
- package/src/__tests__/memory-extra-ops.test.ts +352 -0
- package/src/__tests__/orchestrate-ops.test.ts +284 -0
- package/src/__tests__/planner.test.ts +331 -0
- package/src/__tests__/planning-extra-ops.test.ts +548 -0
- package/src/__tests__/project-ops.test.ts +367 -0
- package/src/__tests__/runtime.test.ts +13 -11
- package/src/__tests__/vault-extra-ops.test.ts +407 -0
- package/src/brain/brain.ts +308 -72
- package/src/brain/intelligence.ts +1230 -0
- package/src/brain/types.ts +214 -0
- package/src/cognee/client.ts +352 -0
- package/src/cognee/types.ts +62 -0
- package/src/control/identity-manager.ts +354 -0
- package/src/control/intent-router.ts +326 -0
- package/src/control/types.ts +102 -0
- package/src/curator/curator.ts +265 -15
- package/src/governance/governance.ts +698 -0
- package/src/governance/index.ts +18 -0
- package/src/governance/types.ts +111 -0
- package/src/index.ts +128 -3
- package/src/llm/llm-client.ts +18 -24
- package/src/logging/logger.ts +154 -0
- package/src/logging/types.ts +21 -0
- package/src/loop/loop-manager.ts +130 -0
- package/src/loop/types.ts +44 -0
- package/src/planning/gap-analysis.ts +506 -0
- package/src/planning/gap-types.ts +58 -0
- package/src/planning/planner.ts +478 -2
- package/src/project/project-registry.ts +358 -0
- package/src/project/types.ts +31 -0
- package/src/runtime/admin-extra-ops.ts +307 -0
- package/src/runtime/admin-ops.ts +329 -0
- package/src/runtime/capture-ops.ts +385 -0
- package/src/runtime/core-ops.ts +747 -26
- package/src/runtime/curator-extra-ops.ts +71 -0
- package/src/runtime/domain-ops.ts +65 -13
- package/src/runtime/grading-ops.ts +121 -0
- package/src/runtime/loop-ops.ts +194 -0
- package/src/runtime/memory-cross-project-ops.ts +192 -0
- package/src/runtime/memory-extra-ops.ts +186 -0
- package/src/runtime/orchestrate-ops.ts +272 -0
- package/src/runtime/planning-extra-ops.ts +327 -0
- package/src/runtime/project-ops.ts +196 -0
- package/src/runtime/runtime.ts +54 -1
- package/src/runtime/types.ts +23 -0
- package/src/runtime/vault-extra-ops.ts +225 -0
- package/src/telemetry/telemetry.ts +118 -0
- package/src/vault/vault.ts +412 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { LoopManager } from '../loop/loop-manager.js';
|
|
3
|
+
import type { LoopConfig } from '../loop/types.js';
|
|
4
|
+
|
|
5
|
+
describe('LoopManager', () => {
|
|
6
|
+
let manager: LoopManager;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
manager = new LoopManager();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// ─── startLoop ─────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
it('starts a loop and returns initial state', () => {
|
|
15
|
+
const config: LoopConfig = {
|
|
16
|
+
mode: 'token-migration',
|
|
17
|
+
prompt: 'Migrate all hardcoded colors to semantic tokens',
|
|
18
|
+
maxIterations: 20,
|
|
19
|
+
targetScore: 95,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const state = manager.startLoop(config);
|
|
23
|
+
|
|
24
|
+
expect(state.id).toMatch(/^loop-\d+$/);
|
|
25
|
+
expect(state.config).toEqual(config);
|
|
26
|
+
expect(state.iterations).toEqual([]);
|
|
27
|
+
expect(state.status).toBe('active');
|
|
28
|
+
expect(state.startedAt).toBeTruthy();
|
|
29
|
+
expect(state.completedAt).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('throws if a loop is already active', () => {
|
|
33
|
+
manager.startLoop({
|
|
34
|
+
mode: 'custom',
|
|
35
|
+
prompt: 'first loop',
|
|
36
|
+
maxIterations: 5,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(() =>
|
|
40
|
+
manager.startLoop({
|
|
41
|
+
mode: 'custom',
|
|
42
|
+
prompt: 'second loop',
|
|
43
|
+
maxIterations: 5,
|
|
44
|
+
}),
|
|
45
|
+
).toThrow(/Loop already active/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ─── isActive ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
it('reports active state correctly', () => {
|
|
51
|
+
expect(manager.isActive()).toBe(false);
|
|
52
|
+
|
|
53
|
+
manager.startLoop({
|
|
54
|
+
mode: 'contrast-fix',
|
|
55
|
+
prompt: 'Fix contrast',
|
|
56
|
+
maxIterations: 10,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(manager.isActive()).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── iterate ───────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
it('records iterations with incrementing numbers', () => {
|
|
65
|
+
manager.startLoop({
|
|
66
|
+
mode: 'component-build',
|
|
67
|
+
prompt: 'Build button',
|
|
68
|
+
maxIterations: 20,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const iter1 = manager.iterate({ passed: false, validationScore: 60 });
|
|
72
|
+
expect(iter1.iteration).toBe(1);
|
|
73
|
+
expect(iter1.passed).toBe(false);
|
|
74
|
+
expect(iter1.validationScore).toBe(60);
|
|
75
|
+
expect(iter1.timestamp).toBeTruthy();
|
|
76
|
+
|
|
77
|
+
const iter2 = manager.iterate({
|
|
78
|
+
passed: true,
|
|
79
|
+
validationScore: 95,
|
|
80
|
+
validationResult: 'All checks pass',
|
|
81
|
+
});
|
|
82
|
+
expect(iter2.iteration).toBe(2);
|
|
83
|
+
expect(iter2.passed).toBe(true);
|
|
84
|
+
expect(iter2.validationResult).toBe('All checks pass');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('throws if iterating with no active loop', () => {
|
|
88
|
+
expect(() => manager.iterate({ passed: true })).toThrow(/No active loop/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('auto-closes loop on max iterations with failing result', () => {
|
|
92
|
+
manager.startLoop({
|
|
93
|
+
mode: 'custom',
|
|
94
|
+
prompt: 'Limited loop',
|
|
95
|
+
maxIterations: 3,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
manager.iterate({ passed: false, validationScore: 30 });
|
|
99
|
+
manager.iterate({ passed: false, validationScore: 50 });
|
|
100
|
+
const iter3 = manager.iterate({ passed: false, validationScore: 70 });
|
|
101
|
+
|
|
102
|
+
expect(iter3.passed).toBe(false);
|
|
103
|
+
expect(manager.isActive()).toBe(false);
|
|
104
|
+
|
|
105
|
+
const history = manager.getHistory();
|
|
106
|
+
expect(history).toHaveLength(1);
|
|
107
|
+
expect(history[0].status).toBe('max-iterations');
|
|
108
|
+
expect(history[0].completedAt).toBeTruthy();
|
|
109
|
+
expect(history[0].iterations).toHaveLength(3);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does NOT auto-close on max iterations if last iteration passes', () => {
|
|
113
|
+
manager.startLoop({
|
|
114
|
+
mode: 'custom',
|
|
115
|
+
prompt: 'Might pass at the end',
|
|
116
|
+
maxIterations: 2,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
manager.iterate({ passed: false, validationScore: 40 });
|
|
120
|
+
// This passes, so auto-close should NOT trigger
|
|
121
|
+
manager.iterate({ passed: true, validationScore: 100 });
|
|
122
|
+
|
|
123
|
+
// Loop should still be active (user must call completeLoop explicitly)
|
|
124
|
+
expect(manager.isActive()).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── completeLoop ──────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
it('completes an active loop', () => {
|
|
130
|
+
manager.startLoop({
|
|
131
|
+
mode: 'plan-iteration',
|
|
132
|
+
prompt: 'Iterate plan to A+',
|
|
133
|
+
maxIterations: 10,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
manager.iterate({ passed: false, validationScore: 70 });
|
|
137
|
+
manager.iterate({ passed: true, validationScore: 95 });
|
|
138
|
+
|
|
139
|
+
const completed = manager.completeLoop();
|
|
140
|
+
|
|
141
|
+
expect(completed.status).toBe('completed');
|
|
142
|
+
expect(completed.completedAt).toBeTruthy();
|
|
143
|
+
expect(completed.iterations).toHaveLength(2);
|
|
144
|
+
expect(manager.isActive()).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('throws if completing with no active loop', () => {
|
|
148
|
+
expect(() => manager.completeLoop()).toThrow(/No active loop to complete/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─── cancelLoop ────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
it('cancels an active loop', () => {
|
|
154
|
+
manager.startLoop({
|
|
155
|
+
mode: 'token-migration',
|
|
156
|
+
prompt: 'Migrate tokens',
|
|
157
|
+
maxIterations: 20,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
manager.iterate({ passed: false, validationScore: 30 });
|
|
161
|
+
|
|
162
|
+
const cancelled = manager.cancelLoop();
|
|
163
|
+
|
|
164
|
+
expect(cancelled.status).toBe('cancelled');
|
|
165
|
+
expect(cancelled.completedAt).toBeTruthy();
|
|
166
|
+
expect(cancelled.iterations).toHaveLength(1);
|
|
167
|
+
expect(manager.isActive()).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('throws if cancelling with no active loop', () => {
|
|
171
|
+
expect(() => manager.cancelLoop()).toThrow(/No active loop to cancel/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ─── getStatus ─────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
it('returns null when no loop is active', () => {
|
|
177
|
+
expect(manager.getStatus()).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns the active loop state', () => {
|
|
181
|
+
const config: LoopConfig = {
|
|
182
|
+
mode: 'contrast-fix',
|
|
183
|
+
prompt: 'Fix contrast',
|
|
184
|
+
maxIterations: 15,
|
|
185
|
+
};
|
|
186
|
+
manager.startLoop(config);
|
|
187
|
+
|
|
188
|
+
const status = manager.getStatus();
|
|
189
|
+
expect(status).not.toBeNull();
|
|
190
|
+
expect(status!.config.mode).toBe('contrast-fix');
|
|
191
|
+
expect(status!.status).toBe('active');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ─── getHistory ────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
it('returns empty history initially', () => {
|
|
197
|
+
expect(manager.getHistory()).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('accumulates completed loops in history', () => {
|
|
201
|
+
// First loop — complete
|
|
202
|
+
manager.startLoop({ mode: 'custom', prompt: 'loop 1', maxIterations: 10 });
|
|
203
|
+
manager.iterate({ passed: true, validationScore: 100 });
|
|
204
|
+
manager.completeLoop();
|
|
205
|
+
|
|
206
|
+
// Second loop — cancel
|
|
207
|
+
manager.startLoop({ mode: 'custom', prompt: 'loop 2', maxIterations: 10 });
|
|
208
|
+
manager.cancelLoop();
|
|
209
|
+
|
|
210
|
+
// Third loop — max-iterations
|
|
211
|
+
manager.startLoop({ mode: 'custom', prompt: 'loop 3', maxIterations: 1 });
|
|
212
|
+
manager.iterate({ passed: false, validationScore: 10 });
|
|
213
|
+
|
|
214
|
+
const history = manager.getHistory();
|
|
215
|
+
expect(history).toHaveLength(3);
|
|
216
|
+
expect(history[0].status).toBe('completed');
|
|
217
|
+
expect(history[1].status).toBe('cancelled');
|
|
218
|
+
expect(history[2].status).toBe('max-iterations');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns a copy of history (not internal array reference)', () => {
|
|
222
|
+
manager.startLoop({ mode: 'custom', prompt: 'test', maxIterations: 10 });
|
|
223
|
+
manager.completeLoop();
|
|
224
|
+
|
|
225
|
+
const history1 = manager.getHistory();
|
|
226
|
+
const history2 = manager.getHistory();
|
|
227
|
+
expect(history1).not.toBe(history2);
|
|
228
|
+
expect(history1).toEqual(history2);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── can start a new loop after completing one ─────────────────
|
|
232
|
+
|
|
233
|
+
it('allows starting a new loop after completing the previous', () => {
|
|
234
|
+
manager.startLoop({ mode: 'custom', prompt: 'first', maxIterations: 5 });
|
|
235
|
+
manager.completeLoop();
|
|
236
|
+
|
|
237
|
+
const second = manager.startLoop({ mode: 'custom', prompt: 'second', maxIterations: 5 });
|
|
238
|
+
expect(second.status).toBe('active');
|
|
239
|
+
expect(manager.isActive()).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('allows starting a new loop after cancelling the previous', () => {
|
|
243
|
+
manager.startLoop({ mode: 'custom', prompt: 'first', maxIterations: 5 });
|
|
244
|
+
manager.cancelLoop();
|
|
245
|
+
|
|
246
|
+
const second = manager.startLoop({ mode: 'custom', prompt: 'second', maxIterations: 5 });
|
|
247
|
+
expect(second.status).toBe('active');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('createLoopOps', () => {
|
|
252
|
+
// Integration-style tests using the ops factory directly.
|
|
253
|
+
// We create a minimal runtime mock with just the loop manager.
|
|
254
|
+
|
|
255
|
+
let manager: LoopManager;
|
|
256
|
+
let ops: Awaited<ReturnType<typeof import('../runtime/loop-ops.js').createLoopOps>>;
|
|
257
|
+
|
|
258
|
+
beforeEach(async () => {
|
|
259
|
+
manager = new LoopManager();
|
|
260
|
+
const { createLoopOps } = await import('../runtime/loop-ops.js');
|
|
261
|
+
// Minimal runtime mock — only `loop` is needed for loop ops
|
|
262
|
+
const mockRuntime = { loop: manager } as import('../runtime/types.js').AgentRuntime;
|
|
263
|
+
ops = createLoopOps(mockRuntime);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
function findOp(name: string) {
|
|
267
|
+
const op = ops.find((o) => o.name === name);
|
|
268
|
+
if (!op) throw new Error(`Op not found: ${name}`);
|
|
269
|
+
return op;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
it('exports 7 loop ops', () => {
|
|
273
|
+
expect(ops).toHaveLength(7);
|
|
274
|
+
const names = ops.map((o) => o.name).sort();
|
|
275
|
+
expect(names).toEqual([
|
|
276
|
+
'loop_cancel',
|
|
277
|
+
'loop_complete',
|
|
278
|
+
'loop_history',
|
|
279
|
+
'loop_is_active',
|
|
280
|
+
'loop_iterate',
|
|
281
|
+
'loop_start',
|
|
282
|
+
'loop_status',
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('loop_start creates a new loop with default max iterations', async () => {
|
|
287
|
+
const op = findOp('loop_start');
|
|
288
|
+
const result = (await op.handler({
|
|
289
|
+
mode: 'token-migration',
|
|
290
|
+
prompt: 'Migrate tokens',
|
|
291
|
+
})) as Record<string, unknown>;
|
|
292
|
+
|
|
293
|
+
expect(result.started).toBe(true);
|
|
294
|
+
expect(result.loopId).toMatch(/^loop-\d+$/);
|
|
295
|
+
expect(result.mode).toBe('token-migration');
|
|
296
|
+
expect(result.maxIterations).toBe(20);
|
|
297
|
+
expect(result.targetScore).toBe(95);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('loop_start uses custom max iterations and target score', async () => {
|
|
301
|
+
const op = findOp('loop_start');
|
|
302
|
+
const result = (await op.handler({
|
|
303
|
+
mode: 'custom',
|
|
304
|
+
prompt: 'Custom task',
|
|
305
|
+
maxIterations: 5,
|
|
306
|
+
targetScore: 80,
|
|
307
|
+
})) as Record<string, unknown>;
|
|
308
|
+
|
|
309
|
+
expect(result.maxIterations).toBe(5);
|
|
310
|
+
expect(result.targetScore).toBe(80);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('loop_iterate records results', async () => {
|
|
314
|
+
await findOp('loop_start').handler({ mode: 'custom', prompt: 'Test' });
|
|
315
|
+
|
|
316
|
+
const result = (await findOp('loop_iterate').handler({
|
|
317
|
+
passed: false,
|
|
318
|
+
validationScore: 50,
|
|
319
|
+
validationResult: 'Needs work',
|
|
320
|
+
})) as Record<string, unknown>;
|
|
321
|
+
|
|
322
|
+
expect(result.iteration).toBe(1);
|
|
323
|
+
expect(result.passed).toBe(false);
|
|
324
|
+
expect(result.validationScore).toBe(50);
|
|
325
|
+
expect(result.loopActive).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('loop_status returns null when no loop active', async () => {
|
|
329
|
+
const result = (await findOp('loop_status').handler({})) as Record<string, unknown>;
|
|
330
|
+
expect(result.active).toBe(false);
|
|
331
|
+
expect(result.loop).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('loop_status returns active loop data', async () => {
|
|
335
|
+
await findOp('loop_start').handler({
|
|
336
|
+
mode: 'contrast-fix',
|
|
337
|
+
prompt: 'Fix contrast issues',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const result = (await findOp('loop_status').handler({})) as Record<string, unknown>;
|
|
341
|
+
expect(result.active).toBe(true);
|
|
342
|
+
const loop = result.loop as Record<string, unknown>;
|
|
343
|
+
expect((loop.config as Record<string, unknown>).mode).toBe('contrast-fix');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('loop_cancel cancels the active loop', async () => {
|
|
347
|
+
await findOp('loop_start').handler({ mode: 'custom', prompt: 'Test' });
|
|
348
|
+
|
|
349
|
+
const result = (await findOp('loop_cancel').handler({})) as Record<string, unknown>;
|
|
350
|
+
expect(result.cancelled).toBe(true);
|
|
351
|
+
expect(result.status).toBe('cancelled');
|
|
352
|
+
|
|
353
|
+
const status = (await findOp('loop_is_active').handler({})) as Record<string, unknown>;
|
|
354
|
+
expect(status.active).toBe(false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('loop_complete marks the loop as completed', async () => {
|
|
358
|
+
await findOp('loop_start').handler({ mode: 'custom', prompt: 'Test' });
|
|
359
|
+
await findOp('loop_iterate').handler({ passed: true, validationScore: 100 });
|
|
360
|
+
|
|
361
|
+
const result = (await findOp('loop_complete').handler({})) as Record<string, unknown>;
|
|
362
|
+
expect(result.completed).toBe(true);
|
|
363
|
+
expect(result.status).toBe('completed');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('loop_history returns past loops', async () => {
|
|
367
|
+
// Complete a loop
|
|
368
|
+
await findOp('loop_start').handler({ mode: 'custom', prompt: 'Loop 1' });
|
|
369
|
+
await findOp('loop_complete').handler({});
|
|
370
|
+
|
|
371
|
+
// Cancel a loop
|
|
372
|
+
await findOp('loop_start').handler({ mode: 'custom', prompt: 'Loop 2' });
|
|
373
|
+
await findOp('loop_cancel').handler({});
|
|
374
|
+
|
|
375
|
+
const result = (await findOp('loop_history').handler({})) as Record<string, unknown>;
|
|
376
|
+
expect(result.count).toBe(2);
|
|
377
|
+
const loops = result.loops as Array<Record<string, unknown>>;
|
|
378
|
+
expect(loops[0].status).toBe('completed');
|
|
379
|
+
expect(loops[1].status).toBe('cancelled');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('loop_is_active returns false when no loop', async () => {
|
|
383
|
+
const result = (await findOp('loop_is_active').handler({})) as Record<string, unknown>;
|
|
384
|
+
expect(result.active).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('assigns correct auth levels', () => {
|
|
388
|
+
const readOps = ['loop_status', 'loop_history', 'loop_is_active'];
|
|
389
|
+
const writeOps = ['loop_start', 'loop_iterate', 'loop_cancel', 'loop_complete'];
|
|
390
|
+
|
|
391
|
+
for (const name of readOps) {
|
|
392
|
+
expect(findOp(name).auth).toBe('read');
|
|
393
|
+
}
|
|
394
|
+
for (const name of writeOps) {
|
|
395
|
+
expect(findOp(name).auth).toBe('write');
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, 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 { createCoreOps } from '../runtime/core-ops.js';
|
|
7
|
+
import type { AgentRuntime } from '../runtime/types.js';
|
|
8
|
+
import type { OpDefinition } from '../facades/types.js';
|
|
9
|
+
|
|
10
|
+
describe('Memory Cross-Project Ops', () => {
|
|
11
|
+
let runtime: AgentRuntime;
|
|
12
|
+
let ops: OpDefinition[];
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = join(tmpdir(), 'cross-project-test-' + Date.now());
|
|
17
|
+
mkdirSync(tempDir, { recursive: true });
|
|
18
|
+
runtime = createAgentRuntime({
|
|
19
|
+
agentId: 'test-cross-project',
|
|
20
|
+
vaultPath: ':memory:',
|
|
21
|
+
plansPath: join(tempDir, 'plans.json'),
|
|
22
|
+
});
|
|
23
|
+
ops = createCoreOps(runtime);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
runtime.close();
|
|
28
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function findOp(name: string): OpDefinition {
|
|
32
|
+
const op = ops.find((o) => o.name === name);
|
|
33
|
+
if (!op) throw new Error(`Op "${name}" not found`);
|
|
34
|
+
return op;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('memory_promote_to_global', () => {
|
|
38
|
+
it('should add _global tag to a vault entry', async () => {
|
|
39
|
+
// Add an entry first
|
|
40
|
+
runtime.vault.add({
|
|
41
|
+
id: 'test-entry-1',
|
|
42
|
+
type: 'pattern',
|
|
43
|
+
domain: 'testing',
|
|
44
|
+
title: 'Test Pattern',
|
|
45
|
+
severity: 'suggestion',
|
|
46
|
+
description: 'A test pattern for cross-project sharing',
|
|
47
|
+
tags: ['test'],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = (await findOp('memory_promote_to_global').handler({
|
|
51
|
+
entryId: 'test-entry-1',
|
|
52
|
+
})) as { promoted: boolean; tags: string[] };
|
|
53
|
+
|
|
54
|
+
expect(result.promoted).toBe(true);
|
|
55
|
+
expect(result.tags).toContain('_global');
|
|
56
|
+
expect(result.tags).toContain('test');
|
|
57
|
+
|
|
58
|
+
// Verify the entry actually has the tag
|
|
59
|
+
const entry = runtime.vault.get('test-entry-1');
|
|
60
|
+
expect(entry!.tags).toContain('_global');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return error for non-existent entry', async () => {
|
|
64
|
+
const result = (await findOp('memory_promote_to_global').handler({
|
|
65
|
+
entryId: 'nonexistent',
|
|
66
|
+
})) as { promoted: boolean; error: string };
|
|
67
|
+
|
|
68
|
+
expect(result.promoted).toBe(false);
|
|
69
|
+
expect(result.error).toContain('not found');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should not duplicate _global tag', async () => {
|
|
73
|
+
runtime.vault.add({
|
|
74
|
+
id: 'test-entry-2',
|
|
75
|
+
type: 'pattern',
|
|
76
|
+
domain: 'testing',
|
|
77
|
+
title: 'Already Global',
|
|
78
|
+
severity: 'suggestion',
|
|
79
|
+
description: 'Already promoted',
|
|
80
|
+
tags: ['_global'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = (await findOp('memory_promote_to_global').handler({
|
|
84
|
+
entryId: 'test-entry-2',
|
|
85
|
+
})) as { promoted: boolean; message: string };
|
|
86
|
+
|
|
87
|
+
expect(result.promoted).toBe(false);
|
|
88
|
+
expect(result.message).toContain('already promoted');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('memory_configure', () => {
|
|
93
|
+
it('should configure cross-project settings', async () => {
|
|
94
|
+
// Register a project first
|
|
95
|
+
runtime.projectRegistry.register('/test/project-a', 'project-a');
|
|
96
|
+
|
|
97
|
+
const result = (await findOp('memory_configure').handler({
|
|
98
|
+
projectPath: '/test/project-a',
|
|
99
|
+
crossProjectEnabled: true,
|
|
100
|
+
extraPaths: ['/test/shared'],
|
|
101
|
+
})) as { configured: boolean; memoryConfig: Record<string, unknown> };
|
|
102
|
+
|
|
103
|
+
expect(result.configured).toBe(true);
|
|
104
|
+
expect(result.memoryConfig.crossProjectEnabled).toBe(true);
|
|
105
|
+
expect(result.memoryConfig.extraPaths).toEqual(['/test/shared']);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return error for unregistered project', async () => {
|
|
109
|
+
const result = (await findOp('memory_configure').handler({
|
|
110
|
+
projectPath: '/unregistered/path',
|
|
111
|
+
crossProjectEnabled: true,
|
|
112
|
+
})) as { configured: boolean; error: string };
|
|
113
|
+
|
|
114
|
+
expect(result.configured).toBe(false);
|
|
115
|
+
expect(result.error).toContain('not registered');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should preserve existing metadata when updating config', async () => {
|
|
119
|
+
runtime.projectRegistry.register('/test/project-b', 'project-b', { custom: 'value' });
|
|
120
|
+
|
|
121
|
+
await findOp('memory_configure').handler({
|
|
122
|
+
projectPath: '/test/project-b',
|
|
123
|
+
crossProjectEnabled: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const project = runtime.projectRegistry.getByPath('/test/project-b');
|
|
127
|
+
expect(project!.metadata.custom).toBe('value');
|
|
128
|
+
expect((project!.metadata.memoryConfig as Record<string, unknown>).crossProjectEnabled).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('memory_cross_project_search', () => {
|
|
133
|
+
it('should search current project memories', async () => {
|
|
134
|
+
runtime.projectRegistry.register('/test/current', 'current');
|
|
135
|
+
|
|
136
|
+
// Capture a memory
|
|
137
|
+
runtime.vault.captureMemory({
|
|
138
|
+
projectPath: '/test/current',
|
|
139
|
+
type: 'lesson',
|
|
140
|
+
context: 'testing',
|
|
141
|
+
summary: 'Always use vitest for TypeScript projects',
|
|
142
|
+
topics: ['testing', 'vitest'],
|
|
143
|
+
filesModified: [],
|
|
144
|
+
toolsUsed: [],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const result = (await findOp('memory_cross_project_search').handler({
|
|
148
|
+
query: 'vitest',
|
|
149
|
+
projectPath: '/test/current',
|
|
150
|
+
})) as { memories: Array<{ weight: number; source: string }>; totalResults: number };
|
|
151
|
+
|
|
152
|
+
expect(result.memories.length).toBeGreaterThanOrEqual(1);
|
|
153
|
+
expect(result.memories[0].weight).toBe(1.0);
|
|
154
|
+
expect(result.memories[0].source).toBe('current');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should include global entries in results', async () => {
|
|
158
|
+
runtime.projectRegistry.register('/test/current', 'current');
|
|
159
|
+
|
|
160
|
+
// Add a global entry
|
|
161
|
+
runtime.vault.add({
|
|
162
|
+
id: 'global-pattern',
|
|
163
|
+
type: 'pattern',
|
|
164
|
+
domain: 'testing',
|
|
165
|
+
title: 'Global Testing Pattern',
|
|
166
|
+
severity: 'suggestion',
|
|
167
|
+
description: 'A globally promoted testing pattern for vitest',
|
|
168
|
+
tags: ['_global', 'testing'],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = (await findOp('memory_cross_project_search').handler({
|
|
172
|
+
query: 'testing',
|
|
173
|
+
projectPath: '/test/current',
|
|
174
|
+
})) as { globalEntries: Array<{ weight: number; source: string }> };
|
|
175
|
+
|
|
176
|
+
expect(result.globalEntries.length).toBeGreaterThanOrEqual(1);
|
|
177
|
+
expect(result.globalEntries[0].weight).toBe(0.9);
|
|
178
|
+
expect(result.globalEntries[0].source).toBe('global');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should search linked project memories', async () => {
|
|
182
|
+
const projA = runtime.projectRegistry.register('/test/project-a', 'project-a');
|
|
183
|
+
const projB = runtime.projectRegistry.register('/test/project-b', 'project-b');
|
|
184
|
+
runtime.projectRegistry.link(projA.id, projB.id, 'related');
|
|
185
|
+
|
|
186
|
+
// Configure cross-project for project A
|
|
187
|
+
await findOp('memory_configure').handler({
|
|
188
|
+
projectPath: '/test/project-a',
|
|
189
|
+
crossProjectEnabled: true,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Capture memory in project B
|
|
193
|
+
runtime.vault.captureMemory({
|
|
194
|
+
projectPath: '/test/project-b',
|
|
195
|
+
type: 'lesson',
|
|
196
|
+
context: 'architecture',
|
|
197
|
+
summary: 'Use facade pattern for clean separation',
|
|
198
|
+
topics: ['architecture', 'facade'],
|
|
199
|
+
filesModified: [],
|
|
200
|
+
toolsUsed: [],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = (await findOp('memory_cross_project_search').handler({
|
|
204
|
+
query: 'facade',
|
|
205
|
+
projectPath: '/test/project-a',
|
|
206
|
+
})) as {
|
|
207
|
+
linkedMemories: Array<{ weight: number; source: string; linkedProject: string }>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
expect(result.linkedMemories.length).toBeGreaterThanOrEqual(1);
|
|
211
|
+
expect(result.linkedMemories[0].weight).toBe(0.8);
|
|
212
|
+
expect(result.linkedMemories[0].source).toBe('linked');
|
|
213
|
+
expect(result.linkedMemories[0].linkedProject).toBe('/test/project-b');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should respect crossProjectEnabled=false', async () => {
|
|
217
|
+
const projA = runtime.projectRegistry.register('/test/disabled-a', 'disabled-a');
|
|
218
|
+
const projB = runtime.projectRegistry.register('/test/disabled-b', 'disabled-b');
|
|
219
|
+
runtime.projectRegistry.link(projA.id, projB.id, 'related');
|
|
220
|
+
|
|
221
|
+
// Explicitly disable cross-project
|
|
222
|
+
await findOp('memory_configure').handler({
|
|
223
|
+
projectPath: '/test/disabled-a',
|
|
224
|
+
crossProjectEnabled: false,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Capture memory in project B
|
|
228
|
+
runtime.vault.captureMemory({
|
|
229
|
+
projectPath: '/test/disabled-b',
|
|
230
|
+
type: 'lesson',
|
|
231
|
+
context: 'testing',
|
|
232
|
+
summary: 'This should not appear in cross-project search',
|
|
233
|
+
topics: ['hidden'],
|
|
234
|
+
filesModified: [],
|
|
235
|
+
toolsUsed: [],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = (await findOp('memory_cross_project_search').handler({
|
|
239
|
+
query: 'hidden',
|
|
240
|
+
projectPath: '/test/disabled-a',
|
|
241
|
+
})) as { linkedMemories: unknown[] };
|
|
242
|
+
|
|
243
|
+
expect(result.linkedMemories).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|