stagent 0.1.10 → 0.1.12
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 +58 -27
- package/package.json +3 -3
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -21
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/costs/page.tsx +53 -43
- package/src/app/globals.css +0 -5
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/page.tsx +5 -0
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +223 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-detail-view.tsx +7 -19
- package/src/components/profiles/profile-form-view.tsx +0 -22
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +32 -28
- package/src/lib/agents/learning-session.ts +234 -0
- package/src/lib/agents/pattern-extractor.ts +34 -64
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +66 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +29 -5
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +4 -2
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -41
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/profile.test.ts +0 -15
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/engine.ts +20 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ─── Mock infrastructure ──────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
mockAll,
|
|
7
|
+
mockLimit,
|
|
8
|
+
mockOrderBy,
|
|
9
|
+
mockWhere,
|
|
10
|
+
mockFrom,
|
|
11
|
+
mockSelect,
|
|
12
|
+
mockValues,
|
|
13
|
+
mockInsert,
|
|
14
|
+
mockSetWhere,
|
|
15
|
+
mockSet,
|
|
16
|
+
mockUpdate,
|
|
17
|
+
} = vi.hoisted(() => {
|
|
18
|
+
const mockAll = vi.fn();
|
|
19
|
+
const mockLimit = vi.fn().mockReturnValue({ all: mockAll });
|
|
20
|
+
const mockOrderBy = vi.fn().mockReturnValue({ limit: mockLimit, all: mockAll });
|
|
21
|
+
const mockWhere = vi.fn().mockReturnValue({ orderBy: mockOrderBy, all: mockAll });
|
|
22
|
+
const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
|
|
23
|
+
const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
|
|
24
|
+
const mockValues = vi.fn().mockResolvedValue(undefined);
|
|
25
|
+
const mockInsert = vi.fn().mockReturnValue({ values: mockValues });
|
|
26
|
+
const mockSetWhere = vi.fn().mockResolvedValue(undefined);
|
|
27
|
+
const mockSet = vi.fn().mockReturnValue({ where: mockSetWhere });
|
|
28
|
+
const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
mockAll,
|
|
32
|
+
mockLimit,
|
|
33
|
+
mockOrderBy,
|
|
34
|
+
mockWhere,
|
|
35
|
+
mockFrom,
|
|
36
|
+
mockSelect,
|
|
37
|
+
mockValues,
|
|
38
|
+
mockInsert,
|
|
39
|
+
mockSetWhere,
|
|
40
|
+
mockSet,
|
|
41
|
+
mockUpdate,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.mock("@/lib/db", () => ({
|
|
46
|
+
db: {
|
|
47
|
+
select: mockSelect,
|
|
48
|
+
insert: mockInsert,
|
|
49
|
+
update: mockUpdate,
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
vi.mock("@/lib/db/schema", () => ({
|
|
54
|
+
learnedContext: {
|
|
55
|
+
profileId: "profile_id",
|
|
56
|
+
version: "version",
|
|
57
|
+
changeType: "change_type",
|
|
58
|
+
content: "content",
|
|
59
|
+
proposalNotificationId: "proposal_notification_id",
|
|
60
|
+
},
|
|
61
|
+
notifications: {
|
|
62
|
+
id: "id",
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
vi.mock("drizzle-orm", () => ({
|
|
67
|
+
eq: vi.fn((_col: string, val: unknown) => ({ col: _col, val })),
|
|
68
|
+
and: vi.fn((...conditions: unknown[]) => conditions),
|
|
69
|
+
desc: vi.fn((col: string) => ({ desc: col })),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// Mock runMetaCompletion for summarization
|
|
73
|
+
const { mockRunMetaCompletion } = vi.hoisted(() => ({
|
|
74
|
+
mockRunMetaCompletion: vi.fn(),
|
|
75
|
+
}));
|
|
76
|
+
vi.mock("../runtime/claude", () => ({
|
|
77
|
+
runMetaCompletion: mockRunMetaCompletion,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// ─── Import under test ────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
import {
|
|
83
|
+
getActiveLearnedContext,
|
|
84
|
+
getContextHistory,
|
|
85
|
+
proposeContextAddition,
|
|
86
|
+
approveProposal,
|
|
87
|
+
rejectProposal,
|
|
88
|
+
rollbackToVersion,
|
|
89
|
+
checkContextSize,
|
|
90
|
+
summarizeContext,
|
|
91
|
+
addDirectContext,
|
|
92
|
+
} from "../learned-context";
|
|
93
|
+
|
|
94
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function resetMockChain() {
|
|
97
|
+
mockAll.mockReset();
|
|
98
|
+
mockLimit.mockReset().mockReturnValue({ all: mockAll });
|
|
99
|
+
mockOrderBy.mockReset().mockReturnValue({ limit: mockLimit, all: mockAll });
|
|
100
|
+
mockWhere.mockReset().mockReturnValue({ orderBy: mockOrderBy, all: mockAll });
|
|
101
|
+
mockFrom.mockReset().mockReturnValue({ where: mockWhere });
|
|
102
|
+
mockSelect.mockReset().mockReturnValue({ from: mockFrom });
|
|
103
|
+
mockValues.mockReset().mockResolvedValue(undefined);
|
|
104
|
+
mockInsert.mockReset().mockReturnValue({ values: mockValues });
|
|
105
|
+
mockSetWhere.mockReset().mockResolvedValue(undefined);
|
|
106
|
+
mockSet.mockReset().mockReturnValue({ where: mockSetWhere });
|
|
107
|
+
mockUpdate.mockReset().mockReturnValue({ set: mockSet });
|
|
108
|
+
mockRunMetaCompletion.mockReset();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
beforeEach(resetMockChain);
|
|
112
|
+
|
|
113
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
114
|
+
// getActiveLearnedContext
|
|
115
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
describe("getActiveLearnedContext", () => {
|
|
118
|
+
it("returns content from the latest approved version", () => {
|
|
119
|
+
mockAll.mockReturnValue([{ content: "Use retry pattern for flaky APIs" }]);
|
|
120
|
+
|
|
121
|
+
const result = getActiveLearnedContext("code-reviewer");
|
|
122
|
+
|
|
123
|
+
expect(result).toBe("Use retry pattern for flaky APIs");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns null when no approved context exists", () => {
|
|
127
|
+
mockAll.mockReturnValue([]);
|
|
128
|
+
|
|
129
|
+
const result = getActiveLearnedContext("general");
|
|
130
|
+
|
|
131
|
+
expect(result).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
136
|
+
// getContextHistory
|
|
137
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
138
|
+
|
|
139
|
+
describe("getContextHistory", () => {
|
|
140
|
+
it("returns all versions ordered by version descending", async () => {
|
|
141
|
+
const rows = [
|
|
142
|
+
{ id: "v3", profileId: "general", version: 3, changeType: "approved" },
|
|
143
|
+
{ id: "v2", profileId: "general", version: 2, changeType: "proposal" },
|
|
144
|
+
{ id: "v1", profileId: "general", version: 1, changeType: "approved" },
|
|
145
|
+
];
|
|
146
|
+
mockAll.mockReturnValue(rows);
|
|
147
|
+
|
|
148
|
+
const result = await getContextHistory("general");
|
|
149
|
+
|
|
150
|
+
expect(result).toEqual(rows);
|
|
151
|
+
expect(result).toHaveLength(3);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
156
|
+
// proposeContextAddition
|
|
157
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
describe("proposeContextAddition", () => {
|
|
160
|
+
it("inserts a proposal row and a notification", async () => {
|
|
161
|
+
// getNextVersion query returns no existing versions
|
|
162
|
+
mockAll.mockReturnValue([]);
|
|
163
|
+
|
|
164
|
+
const notificationId = await proposeContextAddition(
|
|
165
|
+
"code-reviewer",
|
|
166
|
+
"task-42",
|
|
167
|
+
"Always check for null pointers"
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(notificationId).toBeDefined();
|
|
171
|
+
expect(typeof notificationId).toBe("string");
|
|
172
|
+
|
|
173
|
+
// Two inserts: learned_context row + notification
|
|
174
|
+
expect(mockInsert).toHaveBeenCalledTimes(2);
|
|
175
|
+
|
|
176
|
+
// Proposal row has correct shape
|
|
177
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
profileId: "code-reviewer",
|
|
180
|
+
version: 1,
|
|
181
|
+
content: null, // not yet approved
|
|
182
|
+
diff: "Always check for null pointers",
|
|
183
|
+
changeType: "proposal",
|
|
184
|
+
sourceTaskId: "task-42",
|
|
185
|
+
proposedAdditions: "Always check for null pointers",
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Notification has correct type
|
|
190
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
type: "context_proposal",
|
|
193
|
+
title: "Context proposal for code-reviewer",
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("increments version based on existing versions", async () => {
|
|
199
|
+
mockAll.mockReturnValue([{ version: 5 }]);
|
|
200
|
+
|
|
201
|
+
await proposeContextAddition("general", "task-1", "New pattern");
|
|
202
|
+
|
|
203
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
204
|
+
expect.objectContaining({ version: 6 })
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
210
|
+
// approveProposal
|
|
211
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
describe("approveProposal", () => {
|
|
214
|
+
it("merges additions into current context and creates approved version", async () => {
|
|
215
|
+
// First call: find proposal by notification ID
|
|
216
|
+
mockAll
|
|
217
|
+
.mockReturnValueOnce([
|
|
218
|
+
{
|
|
219
|
+
id: "proposal-1",
|
|
220
|
+
profileId: "general",
|
|
221
|
+
proposedAdditions: "New pattern: use early returns",
|
|
222
|
+
sourceTaskId: "task-1",
|
|
223
|
+
},
|
|
224
|
+
])
|
|
225
|
+
// Second call: getActiveLearnedContext (existing context)
|
|
226
|
+
.mockReturnValueOnce([{ content: "Existing pattern: validate inputs" }])
|
|
227
|
+
// Third call: getNextVersion
|
|
228
|
+
.mockReturnValueOnce([{ version: 2 }])
|
|
229
|
+
// Fourth call: checkContextSize -> getActiveLearnedContext
|
|
230
|
+
.mockReturnValueOnce([
|
|
231
|
+
{ content: "Existing pattern: validate inputs\n\nNew pattern: use early returns" },
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
await approveProposal("notif-1");
|
|
235
|
+
|
|
236
|
+
// Approved version inserted with merged content
|
|
237
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
changeType: "approved",
|
|
240
|
+
content: "Existing pattern: validate inputs\n\nNew pattern: use early returns",
|
|
241
|
+
diff: "New pattern: use early returns",
|
|
242
|
+
approvedBy: "human",
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Notification marked as responded
|
|
247
|
+
expect(mockSet).toHaveBeenCalledWith(
|
|
248
|
+
expect.objectContaining({
|
|
249
|
+
response: JSON.stringify({ action: "approved" }),
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("throws if proposal not found", async () => {
|
|
255
|
+
mockAll.mockReturnValue([]);
|
|
256
|
+
|
|
257
|
+
await expect(approveProposal("notif-nonexistent")).rejects.toThrow(
|
|
258
|
+
"Proposal not found"
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("uses editedContent when provided", async () => {
|
|
263
|
+
mockAll
|
|
264
|
+
.mockReturnValueOnce([
|
|
265
|
+
{
|
|
266
|
+
id: "proposal-1",
|
|
267
|
+
profileId: "general",
|
|
268
|
+
proposedAdditions: "Original text",
|
|
269
|
+
sourceTaskId: "task-1",
|
|
270
|
+
},
|
|
271
|
+
])
|
|
272
|
+
.mockReturnValueOnce([]) // no existing context
|
|
273
|
+
.mockReturnValueOnce([]) // no existing versions
|
|
274
|
+
.mockReturnValueOnce([{ content: "Edited by human" }]); // checkContextSize
|
|
275
|
+
|
|
276
|
+
await approveProposal("notif-1", "Edited by human");
|
|
277
|
+
|
|
278
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
279
|
+
expect.objectContaining({
|
|
280
|
+
content: "Edited by human",
|
|
281
|
+
diff: "Edited by human",
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
288
|
+
// rejectProposal
|
|
289
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
290
|
+
|
|
291
|
+
describe("rejectProposal", () => {
|
|
292
|
+
it("creates rejected version and marks notification responded", async () => {
|
|
293
|
+
mockAll
|
|
294
|
+
.mockReturnValueOnce([
|
|
295
|
+
{
|
|
296
|
+
id: "proposal-1",
|
|
297
|
+
profileId: "general",
|
|
298
|
+
proposedAdditions: "Bad pattern",
|
|
299
|
+
sourceTaskId: "task-1",
|
|
300
|
+
},
|
|
301
|
+
])
|
|
302
|
+
// getActiveLearnedContext for preserving current content
|
|
303
|
+
.mockReturnValueOnce([{ content: "Good pattern" }])
|
|
304
|
+
// getNextVersion
|
|
305
|
+
.mockReturnValueOnce([{ version: 3 }]);
|
|
306
|
+
|
|
307
|
+
await rejectProposal("notif-1");
|
|
308
|
+
|
|
309
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
310
|
+
expect.objectContaining({
|
|
311
|
+
changeType: "rejected",
|
|
312
|
+
diff: "Bad pattern",
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(mockSet).toHaveBeenCalledWith(
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
response: JSON.stringify({ action: "rejected" }),
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("throws if proposal not found", async () => {
|
|
324
|
+
mockAll.mockReturnValue([]);
|
|
325
|
+
|
|
326
|
+
await expect(rejectProposal("notif-nonexistent")).rejects.toThrow(
|
|
327
|
+
"Proposal not found"
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
333
|
+
// rollbackToVersion
|
|
334
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
335
|
+
|
|
336
|
+
describe("rollbackToVersion", () => {
|
|
337
|
+
it("creates a new version with the target version's content", async () => {
|
|
338
|
+
mockAll
|
|
339
|
+
// Find target version
|
|
340
|
+
.mockReturnValueOnce([
|
|
341
|
+
{ id: "v2", profileId: "general", version: 2, content: "Version 2 content" },
|
|
342
|
+
])
|
|
343
|
+
// getNextVersion
|
|
344
|
+
.mockReturnValueOnce([{ version: 5 }]);
|
|
345
|
+
|
|
346
|
+
await rollbackToVersion("general", 2);
|
|
347
|
+
|
|
348
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
profileId: "general",
|
|
351
|
+
version: 6,
|
|
352
|
+
content: "Version 2 content",
|
|
353
|
+
diff: "Rolled back to version 2",
|
|
354
|
+
changeType: "rollback",
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("throws if target version not found", async () => {
|
|
360
|
+
mockAll.mockReturnValue([]);
|
|
361
|
+
|
|
362
|
+
await expect(rollbackToVersion("general", 999)).rejects.toThrow(
|
|
363
|
+
"Version 999 not found"
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
369
|
+
// checkContextSize
|
|
370
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
371
|
+
|
|
372
|
+
describe("checkContextSize", () => {
|
|
373
|
+
it("reports size and summarization need", () => {
|
|
374
|
+
mockAll.mockReturnValue([{ content: "x".repeat(7000) }]);
|
|
375
|
+
|
|
376
|
+
const result = checkContextSize("general");
|
|
377
|
+
|
|
378
|
+
expect(result.currentSize).toBe(7000);
|
|
379
|
+
expect(result.limit).toBe(8000);
|
|
380
|
+
expect(result.needsSummarization).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("reports no summarization needed for small context", () => {
|
|
384
|
+
mockAll.mockReturnValue([{ content: "small" }]);
|
|
385
|
+
|
|
386
|
+
const result = checkContextSize("general");
|
|
387
|
+
|
|
388
|
+
expect(result.currentSize).toBe(5);
|
|
389
|
+
expect(result.needsSummarization).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("handles no context gracefully", () => {
|
|
393
|
+
mockAll.mockReturnValue([]);
|
|
394
|
+
|
|
395
|
+
const result = checkContextSize("general");
|
|
396
|
+
|
|
397
|
+
expect(result.currentSize).toBe(0);
|
|
398
|
+
expect(result.needsSummarization).toBe(false);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
403
|
+
// summarizeContext
|
|
404
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
405
|
+
|
|
406
|
+
describe("summarizeContext", () => {
|
|
407
|
+
it("calls runMetaCompletion to summarize and inserts a summarization version", async () => {
|
|
408
|
+
const longContent = "x".repeat(7000);
|
|
409
|
+
mockAll
|
|
410
|
+
// getActiveLearnedContext
|
|
411
|
+
.mockReturnValueOnce([{ content: longContent }])
|
|
412
|
+
// getNextVersion
|
|
413
|
+
.mockReturnValueOnce([{ version: 4 }]);
|
|
414
|
+
|
|
415
|
+
mockRunMetaCompletion.mockResolvedValue({
|
|
416
|
+
text: "condensed version",
|
|
417
|
+
usage: {},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await summarizeContext("general");
|
|
421
|
+
|
|
422
|
+
expect(mockRunMetaCompletion).toHaveBeenCalledOnce();
|
|
423
|
+
expect(mockRunMetaCompletion).toHaveBeenCalledWith(
|
|
424
|
+
expect.objectContaining({ activityType: "context_summarization" })
|
|
425
|
+
);
|
|
426
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
changeType: "summarization",
|
|
429
|
+
content: "condensed version",
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("skips summarization when content is below threshold", async () => {
|
|
435
|
+
mockAll.mockReturnValue([{ content: "short" }]);
|
|
436
|
+
|
|
437
|
+
await summarizeContext("general");
|
|
438
|
+
|
|
439
|
+
expect(mockRunMetaCompletion).not.toHaveBeenCalled();
|
|
440
|
+
expect(mockInsert).not.toHaveBeenCalled();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("skips if summarized text is longer than original", async () => {
|
|
444
|
+
const longContent = "x".repeat(7000);
|
|
445
|
+
mockAll.mockReturnValueOnce([{ content: longContent }]);
|
|
446
|
+
|
|
447
|
+
mockRunMetaCompletion.mockResolvedValue({
|
|
448
|
+
text: "x".repeat(8000),
|
|
449
|
+
usage: {},
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await summarizeContext("general");
|
|
453
|
+
|
|
454
|
+
// No insert because summarized is longer
|
|
455
|
+
expect(mockInsert).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
460
|
+
// addDirectContext
|
|
461
|
+
// ═════════════════════════════════════════════════════════════════════
|
|
462
|
+
|
|
463
|
+
describe("addDirectContext", () => {
|
|
464
|
+
it("merges new content with existing and creates approved version", async () => {
|
|
465
|
+
mockAll
|
|
466
|
+
// getActiveLearnedContext
|
|
467
|
+
.mockReturnValueOnce([{ content: "Existing" }])
|
|
468
|
+
// getNextVersion
|
|
469
|
+
.mockReturnValueOnce([{ version: 1 }])
|
|
470
|
+
// checkContextSize -> getActiveLearnedContext
|
|
471
|
+
.mockReturnValueOnce([{ content: "Existing\n\nNew pattern" }]);
|
|
472
|
+
|
|
473
|
+
await addDirectContext("general", "New pattern");
|
|
474
|
+
|
|
475
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
476
|
+
expect.objectContaining({
|
|
477
|
+
content: "Existing\n\nNew pattern",
|
|
478
|
+
diff: "New pattern",
|
|
479
|
+
changeType: "approved",
|
|
480
|
+
approvedBy: "human",
|
|
481
|
+
})
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("handles first context addition (no existing content)", async () => {
|
|
486
|
+
mockAll
|
|
487
|
+
.mockReturnValueOnce([]) // no existing context
|
|
488
|
+
.mockReturnValueOnce([]) // no existing versions
|
|
489
|
+
.mockReturnValueOnce([{ content: "First pattern" }]); // checkContextSize
|
|
490
|
+
|
|
491
|
+
await addDirectContext("general", "First pattern");
|
|
492
|
+
|
|
493
|
+
expect(mockValues).toHaveBeenCalledWith(
|
|
494
|
+
expect.objectContaining({
|
|
495
|
+
content: "First pattern",
|
|
496
|
+
version: 1,
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
});
|