@cursor/july 0.1.23 → 0.1.24

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.
Files changed (174) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/docs/404.html +1 -1
  3. package/dist/docs/ab.html +2 -2
  4. package/dist/docs/assets/{app.DYcC9FY-.js → app.CvSGaAxk.js} +1 -1
  5. package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +1 -0
  6. package/dist/docs/assets/chunks/{VPLocalSearchBox.o4N_knTV.js → VPLocalSearchBox.J1jJbCvs.js} +1 -1
  7. package/dist/docs/assets/chunks/{theme.DQ-njyo0.js → theme.C7vfzr7h.js} +2 -2
  8. package/dist/docs/building-with-agents.html +2 -2
  9. package/dist/docs/concepts.html +2 -2
  10. package/dist/docs/deployment.html +2 -2
  11. package/dist/docs/evals.html +2 -2
  12. package/dist/docs/example-agents/approval-buddy.html +2 -2
  13. package/dist/docs/example-agents/benny.html +2 -2
  14. package/dist/docs/example-agents/bugbot.html +2 -2
  15. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  16. package/dist/docs/example-agents/codeowners-review.html +2 -2
  17. package/dist/docs/example-agents/concierge.html +2 -2
  18. package/dist/docs/example-agents/fsd.html +2 -2
  19. package/dist/docs/example-agents/index.html +2 -2
  20. package/dist/docs/example-agents/knowledge-base.html +2 -2
  21. package/dist/docs/example-agents/oncall.html +2 -2
  22. package/dist/docs/example-agents/security-reviewer.html +2 -2
  23. package/dist/docs/example-agents/slack-agent.html +2 -2
  24. package/dist/docs/example-agents/weather-agent.html +2 -2
  25. package/dist/docs/guides/agent-to-agent.html +2 -2
  26. package/dist/docs/guides/cloud-runtime.html +2 -2
  27. package/dist/docs/guides/github.html +2 -2
  28. package/dist/docs/guides/human-in-the-loop.html +2 -2
  29. package/dist/docs/guides/mcp-oauth.html +2 -2
  30. package/dist/docs/guides/slack.html +2 -2
  31. package/dist/docs/guides/webhooks.html +2 -2
  32. package/dist/docs/hillclimbing.html +2 -2
  33. package/dist/docs/index.html +2 -2
  34. package/dist/docs/quickstart.html +2 -2
  35. package/dist/docs/reference/agent-config.html +2 -2
  36. package/dist/docs/reference/channels.html +2 -2
  37. package/dist/docs/reference/cli.html +2 -2
  38. package/dist/docs/reference/connections.html +2 -2
  39. package/dist/docs/reference/hooks.html +2 -2
  40. package/dist/docs/reference/http-api.html +2 -2
  41. package/dist/docs/reference/instructions.html +2 -2
  42. package/dist/docs/reference/playground.html +2 -2
  43. package/dist/docs/reference/project-layout.html +2 -2
  44. package/dist/docs/reference/prompt.html +2 -2
  45. package/dist/docs/reference/schedules.html +2 -2
  46. package/dist/docs/reference/sessions.html +2 -2
  47. package/dist/docs/reference/skills.html +2 -2
  48. package/dist/docs/reference/subagents.html +2 -2
  49. package/dist/docs/reference/tools.html +2 -2
  50. package/dist/docs/scaffolding-agents.html +2 -2
  51. package/dist/docs/storage.html +2 -2
  52. package/dist/docs/troubleshooting.html +2 -2
  53. package/dist/playground/assets/index-CidizGZv.css +1 -0
  54. package/dist/playground/assets/{index-DqXdAFGa.js → index-DTG9OsPV.js} +41 -41
  55. package/dist/playground/index.html +2 -2
  56. package/package.json +24 -24
  57. package/dist/channels/github/instrument.d.ts +0 -20
  58. package/dist/channels/github/instrument.d.ts.map +0 -1
  59. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +0 -1
  60. package/dist/internal/json-dir-store.d.ts +0 -32
  61. package/dist/internal/json-dir-store.d.ts.map +0 -1
  62. package/dist/internal/json-dir-store.js +0 -100
  63. package/dist/playground/assets/index-CjOQ4hN9.css +0 -1
  64. package/src/bin/agent-serve.version.test.ts +0 -64
  65. package/src/channels/github/api.test.ts +0 -64
  66. package/src/channels/github/auth.test.ts +0 -105
  67. package/src/channels/github/cursor-account.test.ts +0 -204
  68. package/src/channels/github/forward.test.ts +0 -457
  69. package/src/channels/github/github.test.ts +0 -937
  70. package/src/channels/github/replay.test.ts +0 -179
  71. package/src/channels/slack/api.post-message.test.ts +0 -148
  72. package/src/channels/slack/approvals.test.ts +0 -328
  73. package/src/channels/slack/block-actions.test.ts +0 -452
  74. package/src/channels/slack/bot-mentions.test.ts +0 -267
  75. package/src/channels/slack/channel-watch.test.ts +0 -363
  76. package/src/channels/slack/cursor-account.test.ts +0 -253
  77. package/src/channels/slack/defaults.final-post.test.ts +0 -182
  78. package/src/channels/slack/dispatch.test.ts +0 -795
  79. package/src/channels/slack/eval-directive.test.ts +0 -273
  80. package/src/channels/slack/message-body.test.ts +0 -54
  81. package/src/channels/slack/nudge-store.test.ts +0 -143
  82. package/src/channels/slack/slack.test.ts +0 -391
  83. package/src/channels/slack/stop.test.ts +0 -23
  84. package/src/channels/slack/thread-context.test.ts +0 -202
  85. package/src/evals/assertions.test.ts +0 -580
  86. package/src/evals/expect.test.ts +0 -144
  87. package/src/evals/judge.test.ts +0 -181
  88. package/src/evals/loaders.test.ts +0 -132
  89. package/src/evals/matchers.test.ts +0 -95
  90. package/src/evals/reporters.test.ts +0 -303
  91. package/src/evals/run-facts.test.ts +0 -259
  92. package/src/internal/ab-snapshot.test.ts +0 -325
  93. package/src/internal/approval-gate.test.ts +0 -49
  94. package/src/internal/approvals.integration.test.ts +0 -383
  95. package/src/internal/authored-loaders.test.ts +0 -31
  96. package/src/internal/builtin-tools/reminders.test.ts +0 -201
  97. package/src/internal/channel-route-schema.test.ts +0 -294
  98. package/src/internal/chat-attach.test.ts +0 -262
  99. package/src/internal/cli-deploy.test.ts +0 -1991
  100. package/src/internal/cli-docs.test.ts +0 -161
  101. package/src/internal/cli-mcp.test.ts +0 -789
  102. package/src/internal/cli-skills.test.ts +0 -133
  103. package/src/internal/cli-slack.test.ts +0 -1647
  104. package/src/internal/cloud-merge.test.ts +0 -74
  105. package/src/internal/cron.test.ts +0 -22
  106. package/src/internal/cursor/account-mcp.test.ts +0 -807
  107. package/src/internal/cursor/backend-client.test.ts +0 -591
  108. package/src/internal/cursor/credentials.test.ts +0 -351
  109. package/src/internal/cursor/github-credentials.test.ts +0 -136
  110. package/src/internal/cursor-account-mcp-auth.test.ts +0 -310
  111. package/src/internal/cursor-account.integration.test.ts +0 -441
  112. package/src/internal/cursor-event-relay.test.ts +0 -746
  113. package/src/internal/cursor-github-credentials.integration.test.ts +0 -271
  114. package/src/internal/cursor-slack-relay.test.ts +0 -525
  115. package/src/internal/deploy-source.test.ts +0 -111
  116. package/src/internal/discovery.builtin-tools.test.ts +0 -94
  117. package/src/internal/discovery.concurrency.test.ts +0 -60
  118. package/src/internal/discovery.cursor-account.test.ts +0 -133
  119. package/src/internal/discovery.cwd.test.ts +0 -83
  120. package/src/internal/discovery.hosting.test.ts +0 -80
  121. package/src/internal/discovery.identity.test.ts +0 -44
  122. package/src/internal/docs-site.test.ts +0 -66
  123. package/src/internal/duration.test.ts +0 -29
  124. package/src/internal/eval-judge-model.test.ts +0 -187
  125. package/src/internal/eval-run-store.cancel.test.ts +0 -142
  126. package/src/internal/eval-run-store.storage.test.ts +0 -211
  127. package/src/internal/eval-runner.http.test.ts +0 -403
  128. package/src/internal/eval-runner.run.test.ts +0 -928
  129. package/src/internal/evals-client.test.ts +0 -307
  130. package/src/internal/event-mapper.test.ts +0 -243
  131. package/src/internal/github-fanout.test.ts +0 -213
  132. package/src/internal/handleAgentServeTrigger.test.ts +0 -179
  133. package/src/internal/host-kv.test.ts +0 -82
  134. package/src/internal/host-platforms.test.ts +0 -126
  135. package/src/internal/http-channel.test.ts +0 -402
  136. package/src/internal/init-project.test.ts +0 -270
  137. package/src/internal/install-cursor-skills.test.ts +0 -262
  138. package/src/internal/local-env.test.ts +0 -120
  139. package/src/internal/log-ring.test.ts +0 -31
  140. package/src/internal/logs-client.test.ts +0 -350
  141. package/src/internal/mcp-endpoint.test.ts +0 -436
  142. package/src/internal/mcp-host.test.ts +0 -298
  143. package/src/internal/mcp-oauth.test.ts +0 -148
  144. package/src/internal/net.test.ts +0 -17
  145. package/src/internal/peer-connections.test.ts +0 -128
  146. package/src/internal/peer-mcp.integration.test.ts +0 -289
  147. package/src/internal/playground/toolchain.test.ts +0 -53
  148. package/src/internal/playground-cli.test.ts +0 -187
  149. package/src/internal/playground-proxy.test.ts +0 -376
  150. package/src/internal/prompt-context.integration.test.ts +0 -232
  151. package/src/internal/prompt-context.test.ts +0 -127
  152. package/src/internal/reminder-runner.test.ts +0 -390
  153. package/src/internal/reminder-store.test.ts +0 -53
  154. package/src/internal/request-headers.test.ts +0 -27
  155. package/src/internal/resolve-prod-target.test.ts +0 -787
  156. package/src/internal/resolved-connections.test.ts +0 -295
  157. package/src/internal/router.test.ts +0 -57
  158. package/src/internal/sdk-runner.test.ts +0 -290
  159. package/src/internal/session-engine.coalesce.test.ts +0 -169
  160. package/src/internal/session-engine.concurrency.test.ts +0 -250
  161. package/src/internal/session-engine.host-oauth-mcp.test.ts +0 -110
  162. package/src/internal/session-engine.interrupt.test.ts +0 -577
  163. package/src/internal/session-engine.storage.test.ts +0 -547
  164. package/src/internal/session-urls.test.ts +0 -28
  165. package/src/internal/sessions-client.test.ts +0 -518
  166. package/src/internal/storage-coordinator.test.ts +0 -517
  167. package/src/internal/tool-call.test.ts +0 -458
  168. package/src/internal/tool-result.test.ts +0 -52
  169. package/src/internal/trajectory.approvals.test.ts +0 -83
  170. package/src/internal/trajectory.subagents.test.ts +0 -198
  171. package/src/internal/turn-governor.test.ts +0 -137
  172. package/src/internal/update-check.test.ts +0 -485
  173. package/src/internal/workspace.test.ts +0 -207
  174. package/src/storage-backends/cursor-hosted.test.ts +0 -121
@@ -1,517 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import type { ABMetricSample } from "../ab.js";
3
- import type { EvalRunSnapshot } from "../evals.js";
4
- import type { StorageContext } from "../storage.js";
5
- import { defineStorage, storageKeys } from "../storage.js";
6
- import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
7
- import type { ABSnapshot } from "./ab-snapshot.js";
8
- import type { ReminderRecord } from "./reminder-store.js";
9
- import { StorageCoordinator } from "./storage-coordinator.js";
10
-
11
- function makeRecord(
12
- sessionId: string,
13
- turnCount = 0,
14
- continuationKey: string | null = null
15
- ): SessionRecord {
16
- const now = new Date().toISOString();
17
- return {
18
- sessionId,
19
- channelId: "http",
20
- mode: "chat",
21
- continuationKey,
22
- auth: null,
23
- turnCount,
24
- createdAt: now,
25
- updatedAt: now,
26
- };
27
- }
28
-
29
- let eventIndex = 0;
30
- function makeEvent(
31
- sessionId: string,
32
- type: SessionEvent["type"]
33
- ): SessionEvent {
34
- return {
35
- sessionId,
36
- index: eventIndex++,
37
- at: new Date().toISOString(),
38
- type,
39
- data: {},
40
- } as SessionEvent;
41
- }
42
-
43
- interface Call {
44
- op: "put" | "delete";
45
- key: string;
46
- value?: JsonValue;
47
- ctx: StorageContext;
48
- }
49
-
50
- function makeCoordinator(options?: {
51
- policy?: Parameters<typeof defineStorage>[0]["policy"];
52
- put?: (key: string, value: JsonValue) => void | Promise<void>;
53
- }): {
54
- coordinator: StorageCoordinator;
55
- calls: Call[];
56
- store: Map<string, JsonValue>;
57
- } {
58
- const calls: Call[] = [];
59
- const store = new Map<string, JsonValue>();
60
- const definition = defineStorage({
61
- policy: options?.policy,
62
- put: async (key, value, ctx) => {
63
- calls.push({ op: "put", key, value, ctx });
64
- await options?.put?.(key, value);
65
- store.set(key, value);
66
- },
67
- delete: (key, ctx) => {
68
- calls.push({ op: "delete", key, ctx });
69
- store.delete(key);
70
- },
71
- get: (key) => store.get(key),
72
- list: (prefix) =>
73
- [...store.entries()]
74
- .filter(([key]) => key.startsWith(prefix))
75
- .sort(([a], [b]) => (a < b ? -1 : 1))
76
- .map(([key, value]) => ({ key, value })),
77
- });
78
- const coordinator = new StorageCoordinator({
79
- definition,
80
- agentName: "test-agent",
81
- projectRoot: "/tmp/test-agent",
82
- logger: () => {},
83
- });
84
- return { coordinator, calls, store };
85
- }
86
-
87
- /**
88
- * Append + dispatch-settled, as the engine drives it: the boundary flush
89
- * only fires from eventDispatched, after the event's handlers ran.
90
- */
91
- function emit(coordinator: StorageCoordinator, event: SessionEvent): void {
92
- coordinator.event(event);
93
- coordinator.eventDispatched(event);
94
- }
95
-
96
- describe("StorageCoordinator", () => {
97
- it("buffers events and coalesced records until a turn boundary", async () => {
98
- const { coordinator, calls } = makeCoordinator();
99
-
100
- coordinator.sessionRecord(makeRecord("s1", 0));
101
- const first = makeEvent("s1", "session.started");
102
- emit(coordinator, first);
103
- emit(coordinator, makeEvent("s1", "message.received"));
104
- coordinator.sessionRecord(makeRecord("s1", 1));
105
- await coordinator.whenIdle();
106
- expect(calls).toHaveLength(0);
107
-
108
- emit(coordinator, makeEvent("s1", "turn.completed"));
109
- await coordinator.whenIdle();
110
-
111
- // One chunk put (keyed by the first event's index) + one record put.
112
- expect(calls.map((call) => call.key)).toEqual([
113
- storageKeys.sessionEvents("test-agent", "s1", first.index),
114
- storageKeys.session("test-agent", "s1"),
115
- ]);
116
- const chunk = calls[0]?.value as unknown as SessionEvent[];
117
- expect(chunk.map((event) => event.type)).toEqual([
118
- "session.started",
119
- "message.received",
120
- "turn.completed",
121
- ]);
122
- // Only the newest record survives coalescing.
123
- const record = calls[1]?.value as unknown as SessionRecord;
124
- expect(record.turnCount).toBe(1);
125
- expect(calls[0]?.ctx).toMatchObject({
126
- agentName: "test-agent",
127
- projectRoot: "/tmp/test-agent",
128
- reason: "policy",
129
- });
130
- });
131
-
132
- it("keeps sessions independent", async () => {
133
- const { coordinator, calls } = makeCoordinator();
134
- const s1 = makeEvent("s1", "message.received");
135
- emit(coordinator, s1);
136
- emit(coordinator, makeEvent("s2", "message.received"));
137
- emit(coordinator, makeEvent("s1", "turn.completed"));
138
- await coordinator.whenIdle();
139
-
140
- expect(calls).toHaveLength(1);
141
- expect(calls[0]?.key).toBe(
142
- storageKeys.sessionEvents("test-agent", "s1", s1.index)
143
- );
144
- });
145
-
146
- it("includes record updates from boundary-event handlers in the flush", async () => {
147
- const { coordinator, calls } = makeCoordinator();
148
- const boundary = makeEvent("s1", "turn.completed");
149
- coordinator.event(boundary);
150
- // The boundary event's own handler updates the record (e.g. channel
151
- // state or a new continuation token) before dispatch settles.
152
- coordinator.sessionRecord(makeRecord("s1", 7));
153
- coordinator.eventDispatched(boundary);
154
- await coordinator.whenIdle();
155
-
156
- const record = calls.find(
157
- (call) => call.key === storageKeys.session("test-agent", "s1")
158
- )?.value as unknown as SessionRecord;
159
- expect(record.turnCount).toBe(7);
160
- });
161
-
162
- it("maintains the continuation index across token changes", async () => {
163
- const { coordinator, calls, store } = makeCoordinator();
164
-
165
- coordinator.sessionRecord(makeRecord("s1", 0, "tok-a"));
166
- emit(coordinator, makeEvent("s1", "turn.completed"));
167
- await coordinator.whenIdle();
168
- const keyA = storageKeys.continuation("test-agent", "http", "tok-a");
169
- expect(store.get(keyA)).toEqual({ sessionId: "s1" });
170
-
171
- // Token rotates: the stale index entry is deleted, the new one put.
172
- coordinator.sessionRecord(makeRecord("s1", 1, "tok-b"));
173
- emit(coordinator, makeEvent("s1", "turn.completed"));
174
- await coordinator.whenIdle();
175
- const keyB = storageKeys.continuation("test-agent", "http", "tok-b");
176
- expect(store.has(keyA)).toBe(false);
177
- expect(store.get(keyB)).toEqual({ sessionId: "s1" });
178
- expect(
179
- calls.some((call) => call.op === "delete" && call.key === keyA)
180
- ).toBe(true);
181
-
182
- // Unchanged token on the next flush emits no index writes.
183
- const before = calls.length;
184
- coordinator.sessionRecord(makeRecord("s1", 2, "tok-b"));
185
- emit(coordinator, makeEvent("s1", "turn.completed"));
186
- await coordinator.whenIdle();
187
- const indexWrites = calls
188
- .slice(before)
189
- .filter((call) => call.key.includes("/continuation/"));
190
- expect(indexWrites).toHaveLength(0);
191
- });
192
-
193
- it("bounds oversized continuation tokens in keys and round-trips them", async () => {
194
- const { coordinator, store } = makeCoordinator();
195
- // A hostile caller-supplied token: multi-byte characters inflate the
196
- // URI-encoding well past any sane backend key limit.
197
- const hugeToken = "TOKEN-\u{1F4A5}".repeat(600);
198
- const key = storageKeys.continuation("test-agent", "http", hugeToken);
199
- expect(key).toContain("/continuation/http/sha256:");
200
- expect(Buffer.byteLength(key, "utf8")).toBeLessThan(512);
201
- // Deterministic: the same token always mints the same key.
202
- expect(storageKeys.continuation("test-agent", "http", hugeToken)).toBe(key);
203
-
204
- // Write path (flush) and read path (lazy lookup) agree on the key.
205
- coordinator.sessionRecord(makeRecord("s1", 0, hugeToken));
206
- emit(coordinator, makeEvent("s1", "turn.completed"));
207
- await coordinator.whenIdle();
208
- expect(store.get(key)).toEqual({ sessionId: "s1" });
209
- const record = await coordinator.getSessionByContinuation(
210
- "http",
211
- hugeToken
212
- );
213
- expect(record?.sessionId).toBe("s1");
214
- await expect(coordinator.getSessionById("s1")).resolves.toMatchObject({
215
- sessionId: "s1",
216
- });
217
- await expect(coordinator.getSessionById("ses_missing")).resolves.toBe(
218
- undefined
219
- );
220
- });
221
-
222
- it("drops writes once the delivery queue is full", async () => {
223
- let release: (() => void) | undefined;
224
- const gate = new Promise<void>((resolve) => {
225
- release = resolve;
226
- });
227
- const logs: string[] = [];
228
- const definition = defineStorage({
229
- put: () => gate,
230
- });
231
- const coordinator = new StorageCoordinator({
232
- definition,
233
- agentName: "test-agent",
234
- projectRoot: "/tmp/test-agent",
235
- logger: (line) => logs.push(line),
236
- });
237
-
238
- // The first put blocks the queue; well past the bound, later flushes
239
- // must be shed instead of growing memory without limit.
240
- for (let i = 0; i < 1_200; i += 1) {
241
- emit(coordinator, makeEvent(`s${i}`, "turn.completed"));
242
- }
243
- expect(logs.some((line) => line.includes("storage queue full"))).toBe(true);
244
- release?.();
245
- await coordinator.whenIdle();
246
- });
247
-
248
- it("flushes at the batch cap under a debounce policy", async () => {
249
- const { coordinator, calls } = makeCoordinator({
250
- policy: { debounceMs: 60_000 },
251
- });
252
- for (let i = 0; i < 200; i += 1) {
253
- emit(coordinator, makeEvent("s1", "message.received"));
254
- }
255
- await coordinator.whenIdle();
256
- expect(calls).toHaveLength(1);
257
- expect((calls[0]?.value as unknown as SessionEvent[]).length).toBe(200);
258
- });
259
-
260
- it("debounce spans turn boundaries and carries the coalesced record", async () => {
261
- vi.useFakeTimers();
262
- try {
263
- const { coordinator, calls } = makeCoordinator({
264
- policy: { debounceMs: 1_000 },
265
- });
266
- const first = makeEvent("s1", "message.received");
267
- emit(coordinator, first);
268
- // Boundary events do NOT flush a debounced session.
269
- emit(coordinator, makeEvent("s1", "turn.completed"));
270
- coordinator.sessionRecord(makeRecord("s1", 1));
271
- emit(coordinator, makeEvent("s1", "message.received"));
272
- emit(coordinator, makeEvent("s1", "turn.completed"));
273
- coordinator.sessionRecord(makeRecord("s1", 2));
274
- await coordinator.whenIdle();
275
- expect(calls).toHaveLength(0);
276
-
277
- // Quiet period elapses: both turns land in one chunk, one record.
278
- await vi.advanceTimersByTimeAsync(1_000);
279
- await coordinator.whenIdle();
280
- expect(calls.map((call) => call.key)).toEqual([
281
- storageKeys.sessionEvents("test-agent", "s1", first.index),
282
- storageKeys.session("test-agent", "s1"),
283
- ]);
284
- expect((calls[0]?.value as unknown as SessionEvent[]).length).toBe(4);
285
- expect((calls[1]?.value as unknown as SessionRecord).turnCount).toBe(2);
286
- } finally {
287
- vi.useRealTimers();
288
- }
289
- });
290
-
291
- it("close flushes buffered state with reason shutdown", async () => {
292
- const { coordinator, calls } = makeCoordinator();
293
- coordinator.event(makeEvent("s1", "message.received"));
294
- coordinator.sessionRecord(makeRecord("s1", 1));
295
- await coordinator.close();
296
-
297
- expect(calls.length).toBe(2);
298
- expect(calls.every((call) => call.ctx.reason === "shutdown")).toBe(true);
299
- // Writes after close are ignored.
300
- emit(coordinator, makeEvent("s1", "turn.completed"));
301
- await coordinator.whenIdle();
302
- expect(calls.length).toBe(2);
303
- });
304
-
305
- it("serializes writes and survives a throwing sink", async () => {
306
- const order: string[] = [];
307
- const { coordinator, calls } = makeCoordinator({
308
- put: async (key) => {
309
- order.push(`start ${key}`);
310
- if (order.length === 1) {
311
- await new Promise((resolve) => setTimeout(resolve, 5));
312
- order.push(`end ${key}`);
313
- throw new Error("sink down");
314
- }
315
- order.push(`end ${key}`);
316
- },
317
- });
318
- const first = makeEvent("s1", "turn.completed");
319
- emit(coordinator, first);
320
- emit(coordinator, makeEvent("s2", "turn.completed"));
321
- await coordinator.whenIdle();
322
-
323
- // Serialized (no interleaving), and the throw dropped only its write.
324
- expect(order).toEqual([
325
- `start ${storageKeys.sessionEvents("test-agent", "s1", first.index)}`,
326
- `end ${storageKeys.sessionEvents("test-agent", "s1", first.index)}`,
327
- `start ${calls[1]?.key}`,
328
- `end ${calls[1]?.key}`,
329
- ]);
330
- expect(calls).toHaveLength(2);
331
- });
332
-
333
- it("mirrors reminders onto the sink and lists them back", async () => {
334
- const { coordinator, store } = makeCoordinator();
335
- const now = new Date().toISOString();
336
- const record: ReminderRecord = {
337
- version: 1,
338
- id: "rem_1",
339
- purpose: "watch",
340
- channelId: "drive",
341
- continuationToken: "tok",
342
- scheduleKind: "every",
343
- scheduleValue: "30m",
344
- intervalMs: 30 * 60_000,
345
- prompt: "check",
346
- hasRunHandler: false,
347
- hasUntilFn: false,
348
- busyPolicy: "skipIfBusy",
349
- status: "armed",
350
- fireCount: 0,
351
- nextFireAtMs: Date.now() + 60_000,
352
- createdAt: now,
353
- updatedAt: now,
354
- };
355
- coordinator.reminder(record);
356
- await coordinator.whenIdle();
357
- expect(store.get(storageKeys.reminder("test-agent", "rem_1"))).toEqual(
358
- record
359
- );
360
- await expect(coordinator.listReminders()).resolves.toEqual([record]);
361
-
362
- store.set(storageKeys.reminder("test-agent", "garbage"), {
363
- not: "a reminder",
364
- });
365
- await expect(coordinator.listReminders()).resolves.toEqual([record]);
366
- });
367
-
368
- it("routes eval runs through the dedicated evals table", async () => {
369
- const table = new Map<string, EvalRunSnapshot>();
370
- const contexts: StorageContext[] = [];
371
- const coordinator = new StorageCoordinator({
372
- definition: defineStorage({
373
- put: () => {},
374
- evals: {
375
- put: (run, ctx) => {
376
- contexts.push(ctx);
377
- table.set(run.runId, run);
378
- },
379
- delete: (runId) => {
380
- table.delete(runId);
381
- },
382
- list: () => [...table.values()],
383
- },
384
- }),
385
- agentName: "test-agent",
386
- projectRoot: "/tmp/test-agent",
387
- logger: () => {},
388
- });
389
-
390
- const storage = coordinator.evalRuns();
391
- expect(storage).toBeDefined();
392
- const run = { runId: "run-1", status: "running" } as EvalRunSnapshot;
393
- storage!.save(run);
394
- // Mutations after enqueue must not leak into the delivered snapshot.
395
- run.status = "completed";
396
- await coordinator.whenIdle();
397
- expect(table.get("run-1")?.status).toBe("running");
398
- expect(contexts[0]).toMatchObject({
399
- agentName: "test-agent",
400
- reason: "policy",
401
- });
402
- await expect(storage!.list()).resolves.toHaveLength(1);
403
-
404
- storage!.delete("run-1");
405
- await coordinator.whenIdle();
406
- expect(table.has("run-1")).toBe(false);
407
- });
408
-
409
- it("routes A/B samples and the throttled snapshot through the abs table", async () => {
410
- const samples: ABMetricSample[] = [];
411
- let storedSnapshot: ABSnapshot | undefined;
412
- let snapshotWrites = 0;
413
- const coordinator = new StorageCoordinator({
414
- definition: defineStorage({
415
- put: () => {},
416
- abs: {
417
- putSample: (sample) => {
418
- samples.push(sample);
419
- },
420
- putSnapshot: (snapshot) => {
421
- snapshotWrites += 1;
422
- storedSnapshot = snapshot;
423
- },
424
- getSnapshot: () => storedSnapshot,
425
- },
426
- }),
427
- agentName: "test-agent",
428
- projectRoot: "/tmp/test-agent",
429
- logger: () => {},
430
- });
431
-
432
- const sample = {
433
- experiment: "exp",
434
- variant: "a",
435
- sessionId: "s1",
436
- channelId: "http",
437
- metrics: {},
438
- reason: "turn.completed",
439
- at: "2026-07-15T00:00:00.000Z",
440
- } as unknown as ABMetricSample;
441
- coordinator.abSample(sample);
442
- await coordinator.whenIdle();
443
- expect(samples).toEqual([sample]);
444
-
445
- const snapshot = (id: string) =>
446
- ({
447
- experiments: [],
448
- sessions: [],
449
- config: { id },
450
- }) as unknown as ABSnapshot;
451
- coordinator.abSnapshot(snapshot("first"));
452
- coordinator.abSnapshot(snapshot("suppressed"));
453
- await coordinator.whenIdle();
454
- expect(snapshotWrites).toBe(1);
455
- await expect(coordinator.getLatestAbSnapshot()).resolves.toEqual(
456
- snapshot("first")
457
- );
458
- });
459
-
460
- it("treats missing evals/abs tables as memory-only", async () => {
461
- const { coordinator } = makeCoordinator();
462
- expect(coordinator.evalRuns()).toBeUndefined();
463
- // No abs table: sample/snapshot writes are dropped, reads are absent.
464
- coordinator.abSample({} as ABMetricSample);
465
- coordinator.abSnapshot({} as ABSnapshot);
466
- await coordinator.whenIdle();
467
- await expect(coordinator.getLatestAbSnapshot()).resolves.toBeUndefined();
468
- });
469
-
470
- it("reads empty state from a write-only sink", async () => {
471
- const definition = defineStorage({ put: () => {} });
472
- const coordinator = new StorageCoordinator({
473
- definition,
474
- agentName: "test-agent",
475
- projectRoot: "/tmp/test-agent",
476
- logger: () => {},
477
- });
478
- expect(coordinator.canBulkRestore).toBe(false);
479
- expect(coordinator.supportsAuthorKv).toBe(false);
480
- await expect(coordinator.listSessions()).resolves.toEqual([]);
481
- await expect(coordinator.listSessionEvents("s1")).resolves.toEqual([]);
482
- await expect(
483
- coordinator.getSessionByContinuation("http", "tok")
484
- ).resolves.toBeUndefined();
485
- await expect(coordinator.getLatestAbSnapshot()).resolves.toBeUndefined();
486
- });
487
-
488
- it("awaits author kv get/put/delete under storageKeys.kv", async () => {
489
- const store = new Map<string, unknown>();
490
- const coordinator = new StorageCoordinator({
491
- definition: defineStorage({
492
- put: (key, value) => {
493
- store.set(key, value);
494
- },
495
- get: (key) => store.get(key) as never,
496
- delete: (key) => {
497
- store.delete(key);
498
- },
499
- }),
500
- agentName: "test-agent",
501
- projectRoot: "/tmp/test-agent",
502
- logger: () => {},
503
- });
504
- expect(coordinator.supportsAuthorKv).toBe(true);
505
- await coordinator.kvPut("alert-memory/1", { ok: true });
506
- expect(store.get(storageKeys.kv("test-agent", "alert-memory/1"))).toEqual({
507
- ok: true,
508
- });
509
- await expect(coordinator.kvGet("alert-memory/1")).resolves.toEqual({
510
- ok: true,
511
- });
512
- await coordinator.kvPut("alert-memory/null", null);
513
- await expect(coordinator.kvGet("alert-memory/null")).resolves.toBeNull();
514
- await coordinator.kvDelete("alert-memory/1");
515
- await expect(coordinator.kvGet("alert-memory/1")).resolves.toBeUndefined();
516
- });
517
- });