@app-vitals/agent-runtime 0.0.0-stage → 0.1.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/Dockerfile +265 -0
- package/agent-account-status-ref.ts +53 -0
- package/agent-account-status-ref.unit.test.ts +24 -0
- package/agent-extension.ts +144 -0
- package/agent-extension.unit.test.ts +139 -0
- package/agent-repos-ref.ts +63 -0
- package/agent-repos-ref.unit.test.ts +84 -0
- package/agent-slack-membership-ref.ts +94 -0
- package/agent-slack-membership-ref.unit.test.ts +201 -0
- package/agent-trial-expiry-ref.ts +63 -0
- package/agent-trial-expiry-ref.unit.test.ts +84 -0
- package/anthropic-federation-guard.ts +72 -0
- package/anthropic-federation-guard.unit.test.ts +127 -0
- package/base-image-contract.smoke.test.ts +149 -0
- package/boot.ts +845 -0
- package/browser.integration.test.ts +52 -0
- package/browser.ts +34 -0
- package/chat-poller.integration.test.ts +109 -0
- package/chat-poller.ts +401 -0
- package/chat-poller.unit.test.ts +1581 -0
- package/chat-token-reporter.integration.test.ts +282 -0
- package/chat-token-reporter.ts +136 -0
- package/claude-version.ts +55 -0
- package/claude-version.unit.test.ts +73 -0
- package/claude.ts +1054 -0
- package/claude.unit.test.ts +3042 -0
- package/cleanup-after-days-ref.ts +83 -0
- package/cleanup-after-days-ref.unit.test.ts +89 -0
- package/cleanup-merged-worktrees-ref.ts +75 -0
- package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
- package/cli-args.ts +36 -0
- package/cli-args.unit.test.ts +94 -0
- package/clock.ts +47 -0
- package/config.ts +99 -0
- package/config.unit.test.ts +356 -0
- package/context-stamp.ts +189 -0
- package/context-stamp.unit.test.ts +159 -0
- package/cron-failure-reporter.ts +107 -0
- package/cron-failure-reporter.unit.test.ts +244 -0
- package/cron-handler.integration.test.ts +614 -0
- package/cron-handler.smoke.test.ts +1371 -0
- package/cron-handler.ts +750 -0
- package/cron-handler.unit.test.ts +1177 -0
- package/cron-run-reporter.integration.test.ts +806 -0
- package/cron-run-reporter.ts +398 -0
- package/crypto.ts +73 -0
- package/cutover-validate.integration.test.ts +245 -0
- package/cutover-validate.ts +84 -0
- package/entrypoint-main.ts +79 -0
- package/entrypoint.integration.test.ts +315 -0
- package/entrypoint.ts +154 -0
- package/errors.ts +61 -0
- package/fixtures/chat-service/attachment.json +15 -0
- package/fixtures/chat-service/messages.json +86 -0
- package/fixtures/chat-service/threads.json +50 -0
- package/fixtures/github-installations/installations.json +1231 -0
- package/fixtures/slack-provision-cassette.json +6 -0
- package/fixtures/specialties/fixture/manifest.yaml +35 -0
- package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
- package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
- package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
- package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
- package/fixtures/stream-json/clean-multi-turn.ts +209 -0
- package/fixtures/stream-json/progress-phases.ts +203 -0
- package/fixtures/stream-json/skill-attribution.ts +251 -0
- package/fixtures/stream-json/truncated-no-result.ts +84 -0
- package/format.ts +283 -0
- package/format.unit.test.ts +558 -0
- package/gh-helpers.ts +139 -0
- package/gh-helpers.unit.test.ts +253 -0
- package/gh-token-files.integration.test.ts +143 -0
- package/gh-token-files.ts +92 -0
- package/gh-token-files.unit.test.ts +63 -0
- package/github-app-auth.integration.test.ts +419 -0
- package/github-app-auth.ts +193 -0
- package/github-auth-deps.ts +128 -0
- package/github-auth-startup.ts +231 -0
- package/github-auth-startup.unit.test.ts +325 -0
- package/github-installations-manager.integration.test.ts +323 -0
- package/github-installations-reporter.integration.test.ts +124 -0
- package/github-installations-reporter.ts +150 -0
- package/github-installations-reporter.unit.test.ts +166 -0
- package/github-installations.integration.test.ts +88 -0
- package/github-installations.ts +462 -0
- package/github-installations.unit.test.ts +98 -0
- package/health.smoke.test.ts +244 -0
- package/health.ts +193 -0
- package/health.unit.test.ts +40 -0
- package/http-chat-service-client.integration.test.ts +345 -0
- package/http-chat-service-client.ts +287 -0
- package/http-chat-service-client.unit.test.ts +500 -0
- package/log-prefix.ts +12 -0
- package/log-prefix.unit.test.ts +24 -0
- package/markers.ts +178 -0
- package/markers.unit.test.ts +374 -0
- package/package.json +40 -4
- package/piper-voice.ts +116 -0
- package/piper-voice.unit.test.ts +124 -0
- package/pricing.ts +7 -0
- package/pricing.unit.test.ts +149 -0
- package/process-tree-kill.integration.test.ts +133 -0
- package/process-tree-kill.ts +325 -0
- package/process-tree-kill.unit.test.ts +265 -0
- package/progress-milestones.ts +86 -0
- package/progress-milestones.unit.test.ts +188 -0
- package/run-telemetry.ts +241 -0
- package/run-telemetry.unit.test.ts +235 -0
- package/sessions.integration.test.ts +273 -0
- package/sessions.ts +191 -0
- package/sessions.unit.test.ts +94 -0
- package/setup-github-auth.integration.test.ts +858 -0
- package/setup-github-auth.ts +297 -0
- package/setup.fixture.integration.test.ts +109 -0
- package/setup.integration.test.ts +1495 -0
- package/setup.ts +853 -0
- package/setup.unit.test.ts +286 -0
- package/shipwright-config-client.ts +36 -0
- package/shipwright-runtime-client.integration.test.ts +336 -0
- package/shipwright-runtime-client.ts +118 -0
- package/slack-account-status.unit.test.ts +251 -0
- package/slack-bolt-client.integration.test.ts +76 -0
- package/slack-chat-conversation.integration.test.ts +333 -0
- package/slack-progress.ts +510 -0
- package/slack-progress.unit.test.ts +806 -0
- package/slack-socket-mode-ping.integration.test.ts +137 -0
- package/slack-startup.ts +170 -0
- package/slack-startup.unit.test.ts +300 -0
- package/slack-thread-status-tracker.ts +70 -0
- package/slack-thread-status-tracker.unit.test.ts +118 -0
- package/slack-trial-expiry.unit.test.ts +269 -0
- package/slack.integration.test.ts +3979 -0
- package/slack.ts +1286 -0
- package/slack.unit.test.ts +1070 -0
- package/spawn-env.ts +58 -0
- package/spawn-env.unit.test.ts +96 -0
- package/startup-dm.integration.test.ts +156 -0
- package/startup-dm.ts +29 -0
- package/sync-config-clone.ts +238 -0
- package/sync-config-clone.unit.test.ts +429 -0
- package/test-env.ts +27 -0
- package/users.ts +90 -0
- package/users.unit.test.ts +108 -0
- package/voice.integration.test.ts +669 -0
- package/voice.ts +315 -0
- package/voice.unit.test.ts +105 -0
- package/workspace/BOOTSTRAP.md.template +43 -0
- package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
- package/workspace/IDENTITY.md.template +8 -0
- package/workspace/RUNTIME.md.template +193 -0
- package/workspace/RUNTIME.md.template.content.test.ts +75 -0
- package/workspace/SOUL.md.template +45 -0
- package/workspace/SPECIALTY.md.template +37 -0
- package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
- package/workspace/VOICE.md.template +8 -0
- package/workspace/mise.toml.template +12 -0
- package/workspace/requirements.txt.template +1 -0
- package/workspace-paths.ts +37 -0
- package/worktree-reaper.ts +244 -0
- package/worktree-reaper.unit.test.ts +380 -0
- package/README.md +0 -3
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for HttpCronRunReporter (new two-step interface).
|
|
3
|
+
*
|
|
4
|
+
* Strategy: spin up a real Bun.serve stub admin API, inject its URL, and verify
|
|
5
|
+
* the reporter POSTs / PATCHes the correct payloads — no global.fetch overrides,
|
|
6
|
+
* no mock.module().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
HttpCronRunReporter,
|
|
12
|
+
NoopCronRunReporter,
|
|
13
|
+
} from "./cron-run-reporter.ts";
|
|
14
|
+
|
|
15
|
+
// ─── Shared helpers ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface CapturedRequest {
|
|
18
|
+
url: string;
|
|
19
|
+
method: string;
|
|
20
|
+
headers: Record<string, string>;
|
|
21
|
+
body: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StubState {
|
|
25
|
+
captured: CapturedRequest[];
|
|
26
|
+
postStatusToReturn: number;
|
|
27
|
+
patchStatusToReturn: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function startStubServer(
|
|
31
|
+
port: number,
|
|
32
|
+
state: StubState,
|
|
33
|
+
// biome-ignore lint/suspicious/noExplicitAny: Server type param varies by bun version
|
|
34
|
+
): ReturnType<typeof Bun.serve<any>> {
|
|
35
|
+
return Bun.serve({
|
|
36
|
+
port,
|
|
37
|
+
fetch: async (req) => {
|
|
38
|
+
const body = await req.json().catch(() => null);
|
|
39
|
+
state.captured.push({
|
|
40
|
+
url: req.url,
|
|
41
|
+
method: req.method,
|
|
42
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (req.method === "POST") {
|
|
47
|
+
if (state.postStatusToReturn >= 400) {
|
|
48
|
+
return new Response(JSON.stringify({ error: "stub error" }), {
|
|
49
|
+
status: state.postStatusToReturn,
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return new Response(JSON.stringify({ run: { id: "run-stub-1" } }), {
|
|
54
|
+
status: 201,
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// PATCH
|
|
60
|
+
if (state.patchStatusToReturn >= 400) {
|
|
61
|
+
return new Response(JSON.stringify({ error: "stub error" }), {
|
|
62
|
+
status: state.patchStatusToReturn,
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return new Response(JSON.stringify({ run: { id: "run-stub-1" } }), {
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── HttpCronRunReporter ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("HttpCronRunReporter", () => {
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: Server type param varies by bun version
|
|
78
|
+
let server: ReturnType<typeof Bun.serve<any>>;
|
|
79
|
+
let state: StubState;
|
|
80
|
+
let baseUrl: string;
|
|
81
|
+
const AGENT_ID = "agent-abc";
|
|
82
|
+
const API_KEY = "test-api-key";
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
state = {
|
|
86
|
+
captured: [],
|
|
87
|
+
postStatusToReturn: 201,
|
|
88
|
+
patchStatusToReturn: 200,
|
|
89
|
+
};
|
|
90
|
+
server = startStubServer(0, state);
|
|
91
|
+
baseUrl = `http://localhost:${server.port}`;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(async () => {
|
|
95
|
+
await server.stop(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function makeReporter() {
|
|
99
|
+
return new HttpCronRunReporter({
|
|
100
|
+
apiUrl: baseUrl,
|
|
101
|
+
agentId: AGENT_ID,
|
|
102
|
+
apiKey: API_KEY,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── createRun ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test("createRun POSTs to correct URL: /agents/:agentId/crons/:cronId/runs", async () => {
|
|
109
|
+
const reporter = makeReporter();
|
|
110
|
+
const startedAt = new Date("2026-01-01T08:00:00.000Z");
|
|
111
|
+
|
|
112
|
+
await reporter.createRun("cron-123", startedAt);
|
|
113
|
+
|
|
114
|
+
expect(state.captured).toHaveLength(1);
|
|
115
|
+
expect(state.captured[0].method).toBe("POST");
|
|
116
|
+
expect(state.captured[0].url).toContain(
|
|
117
|
+
`/agents/${AGENT_ID}/crons/cron-123/runs`,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("createRun sends only startedAt in body", async () => {
|
|
122
|
+
const reporter = makeReporter();
|
|
123
|
+
const startedAt = new Date("2026-01-01T08:00:00.000Z");
|
|
124
|
+
|
|
125
|
+
await reporter.createRun("cron-123", startedAt);
|
|
126
|
+
|
|
127
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
128
|
+
expect(body.startedAt).toBe(startedAt.toISOString());
|
|
129
|
+
expect(Object.keys(body)).toEqual(["startedAt"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("createRun returns run.id from response", async () => {
|
|
133
|
+
const reporter = makeReporter();
|
|
134
|
+
const runId = await reporter.createRun(
|
|
135
|
+
"cron-123",
|
|
136
|
+
new Date("2026-01-01T08:00:00.000Z"),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(runId).toBe("run-stub-1");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("createRun sends Authorization header with api key", async () => {
|
|
143
|
+
const reporter = makeReporter();
|
|
144
|
+
await reporter.createRun("cron-auth", new Date());
|
|
145
|
+
|
|
146
|
+
expect(state.captured[0].headers.authorization).toBe(`Bearer ${API_KEY}`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("createRun returns null on network error (does not throw)", async () => {
|
|
150
|
+
const reporter = new HttpCronRunReporter({
|
|
151
|
+
apiUrl: "http://localhost:19999", // nothing listening
|
|
152
|
+
agentId: AGENT_ID,
|
|
153
|
+
apiKey: API_KEY,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const result = await reporter.createRun("cron-net-err", new Date());
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("createRun returns null on HTTP error (does not throw)", async () => {
|
|
161
|
+
state.postStatusToReturn = 500;
|
|
162
|
+
const reporter = makeReporter();
|
|
163
|
+
|
|
164
|
+
const result = await reporter.createRun("cron-http-err", new Date());
|
|
165
|
+
expect(result).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── completeRun ───────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
test("completeRun PATCHes to correct URL: /agents/:agentId/crons/:cronId/runs/:runId", async () => {
|
|
171
|
+
const reporter = makeReporter();
|
|
172
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
173
|
+
|
|
174
|
+
await reporter.completeRun("cron-123", "run-abc", completedAt, "completed");
|
|
175
|
+
|
|
176
|
+
expect(state.captured).toHaveLength(1);
|
|
177
|
+
expect(state.captured[0].method).toBe("PATCH");
|
|
178
|
+
expect(state.captured[0].url).toContain(
|
|
179
|
+
`/agents/${AGENT_ID}/crons/cron-123/runs/run-abc`,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("completeRun sends completedAt + outcome in body", async () => {
|
|
184
|
+
const reporter = makeReporter();
|
|
185
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
186
|
+
|
|
187
|
+
await reporter.completeRun("cron-123", "run-abc", completedAt, "completed");
|
|
188
|
+
|
|
189
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
190
|
+
expect(body.completedAt).toBe(completedAt.toISOString());
|
|
191
|
+
expect(body.outcome).toBe("completed");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("completeRun sends token data when provided", async () => {
|
|
195
|
+
const reporter = makeReporter();
|
|
196
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
197
|
+
|
|
198
|
+
await reporter.completeRun(
|
|
199
|
+
"cron-123",
|
|
200
|
+
"run-abc",
|
|
201
|
+
completedAt,
|
|
202
|
+
"completed",
|
|
203
|
+
{
|
|
204
|
+
inputTokens: 100,
|
|
205
|
+
outputTokens: 50,
|
|
206
|
+
cacheReadTokens: 20,
|
|
207
|
+
cacheCreationTokens: 10,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
212
|
+
expect(body.inputTokens).toBe(100);
|
|
213
|
+
expect(body.outputTokens).toBe(50);
|
|
214
|
+
expect(body.cacheReadTokens).toBe(20);
|
|
215
|
+
expect(body.cacheCreationTokens).toBe(10);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("completeRun sends error when provided (failed outcome)", async () => {
|
|
219
|
+
const reporter = makeReporter();
|
|
220
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
221
|
+
|
|
222
|
+
await reporter.completeRun("cron-123", "run-abc", completedAt, "failed", {
|
|
223
|
+
error: "something went wrong",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
227
|
+
expect(body.outcome).toBe("failed");
|
|
228
|
+
expect(body.error).toBe("something went wrong");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("completeRun does nothing when runId is null", async () => {
|
|
232
|
+
const reporter = makeReporter();
|
|
233
|
+
await reporter.completeRun("cron-123", null, new Date(), "completed");
|
|
234
|
+
|
|
235
|
+
expect(state.captured).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("completeRun swallows network errors (does not throw)", async () => {
|
|
239
|
+
const reporter = new HttpCronRunReporter({
|
|
240
|
+
apiUrl: "http://localhost:19999", // nothing listening
|
|
241
|
+
agentId: AGENT_ID,
|
|
242
|
+
apiKey: API_KEY,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await expect(
|
|
246
|
+
reporter.completeRun("cron-net-err", "run-1", new Date(), "completed"),
|
|
247
|
+
).resolves.toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("completeRun swallows HTTP error responses (does not throw)", async () => {
|
|
251
|
+
state.patchStatusToReturn = 500;
|
|
252
|
+
const reporter = makeReporter();
|
|
253
|
+
|
|
254
|
+
await expect(
|
|
255
|
+
reporter.completeRun("cron-http-err", "run-1", new Date(), "completed"),
|
|
256
|
+
).resolves.toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("completeRun sends modelBreakdown when provided", async () => {
|
|
260
|
+
const reporter = makeReporter();
|
|
261
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
262
|
+
|
|
263
|
+
await reporter.completeRun(
|
|
264
|
+
"cron-123",
|
|
265
|
+
"run-abc",
|
|
266
|
+
completedAt,
|
|
267
|
+
"completed",
|
|
268
|
+
{
|
|
269
|
+
inputTokens: 300,
|
|
270
|
+
outputTokens: 150,
|
|
271
|
+
modelBreakdown: [
|
|
272
|
+
{
|
|
273
|
+
model: "claude-sonnet-4-5",
|
|
274
|
+
inputTokens: 200,
|
|
275
|
+
outputTokens: 100,
|
|
276
|
+
cacheReadTokens: 0,
|
|
277
|
+
cacheCreationTokens: 0,
|
|
278
|
+
costUsd: 0.002,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
model: "claude-haiku-4-5",
|
|
282
|
+
inputTokens: 100,
|
|
283
|
+
outputTokens: 50,
|
|
284
|
+
cacheReadTokens: 0,
|
|
285
|
+
cacheCreationTokens: 0,
|
|
286
|
+
costUsd: 0.001,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
293
|
+
expect(Array.isArray(body.modelBreakdown)).toBe(true);
|
|
294
|
+
const breakdown = body.modelBreakdown as Array<Record<string, unknown>>;
|
|
295
|
+
expect(breakdown).toHaveLength(2);
|
|
296
|
+
expect(breakdown[0].model).toBe("claude-sonnet-4-5");
|
|
297
|
+
expect(breakdown[0].inputTokens).toBe(200);
|
|
298
|
+
expect(breakdown[1].model).toBe("claude-haiku-4-5");
|
|
299
|
+
expect(breakdown[1].inputTokens).toBe(100);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("completeRun sends sessionId when provided", async () => {
|
|
303
|
+
const reporter = makeReporter();
|
|
304
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
305
|
+
|
|
306
|
+
await reporter.completeRun(
|
|
307
|
+
"cron-123",
|
|
308
|
+
"run-abc",
|
|
309
|
+
completedAt,
|
|
310
|
+
"completed",
|
|
311
|
+
{ sessionId: "session-abc-123" },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
315
|
+
expect(body.sessionId).toBe("session-abc-123");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("completeRun omits sessionId when not provided", async () => {
|
|
319
|
+
const reporter = makeReporter();
|
|
320
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
321
|
+
|
|
322
|
+
await reporter.completeRun(
|
|
323
|
+
"cron-123",
|
|
324
|
+
"run-abc",
|
|
325
|
+
completedAt,
|
|
326
|
+
"completed",
|
|
327
|
+
{
|
|
328
|
+
inputTokens: 100,
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
333
|
+
expect(body.sessionId).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ─── recordProgress ────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
test("recordProgress PATCHes to correct URL with modelBreakdown and lastHeartbeatAt in body", async () => {
|
|
339
|
+
const reporter = makeReporter();
|
|
340
|
+
const modelBreakdown = [
|
|
341
|
+
{
|
|
342
|
+
model: "claude-sonnet-4-5",
|
|
343
|
+
inputTokens: 200,
|
|
344
|
+
outputTokens: 100,
|
|
345
|
+
cacheReadTokens: 0,
|
|
346
|
+
cacheCreationTokens: 0,
|
|
347
|
+
costUsd: 0.002,
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
const lastHeartbeatAt = new Date("2026-01-01T08:00:03.000Z");
|
|
351
|
+
|
|
352
|
+
await reporter.recordProgress(
|
|
353
|
+
"cron-123",
|
|
354
|
+
"run-abc",
|
|
355
|
+
modelBreakdown,
|
|
356
|
+
lastHeartbeatAt,
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
expect(state.captured).toHaveLength(1);
|
|
360
|
+
expect(state.captured[0].method).toBe("PATCH");
|
|
361
|
+
expect(state.captured[0].url).toContain(
|
|
362
|
+
`/agents/${AGENT_ID}/crons/cron-123/runs/run-abc`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
366
|
+
expect(new Set(Object.keys(body))).toEqual(
|
|
367
|
+
new Set(["modelBreakdown", "lastHeartbeatAt"]),
|
|
368
|
+
);
|
|
369
|
+
expect(body.completedAt).toBeUndefined();
|
|
370
|
+
expect(body.outcome).toBeUndefined();
|
|
371
|
+
expect(body.lastHeartbeatAt).toBe(lastHeartbeatAt.toISOString());
|
|
372
|
+
const breakdown = body.modelBreakdown as Array<Record<string, unknown>>;
|
|
373
|
+
expect(breakdown).toHaveLength(1);
|
|
374
|
+
expect(breakdown[0].model).toBe("claude-sonnet-4-5");
|
|
375
|
+
expect(breakdown[0].inputTokens).toBe(200);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("recordProgress does nothing when runId is null", async () => {
|
|
379
|
+
const reporter = makeReporter();
|
|
380
|
+
await reporter.recordProgress("cron-123", null, [], new Date());
|
|
381
|
+
|
|
382
|
+
expect(state.captured).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("recordProgress swallows network errors (does not throw)", async () => {
|
|
386
|
+
const reporter = new HttpCronRunReporter({
|
|
387
|
+
apiUrl: "http://localhost:19999", // nothing listening
|
|
388
|
+
agentId: AGENT_ID,
|
|
389
|
+
apiKey: API_KEY,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await expect(
|
|
393
|
+
reporter.recordProgress("cron-net-err", "run-1", [], new Date()),
|
|
394
|
+
).resolves.toBeUndefined();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("recordProgress swallows HTTP error responses (does not throw)", async () => {
|
|
398
|
+
state.patchStatusToReturn = 500;
|
|
399
|
+
const reporter = makeReporter();
|
|
400
|
+
|
|
401
|
+
await expect(
|
|
402
|
+
reporter.recordProgress("cron-http-err", "run-1", [], new Date()),
|
|
403
|
+
).resolves.toBeUndefined();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ─── recordSessionId (DTW-1.3) ─────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
test("recordSessionId PATCHes to correct URL with only sessionId in body", async () => {
|
|
409
|
+
const reporter = makeReporter();
|
|
410
|
+
|
|
411
|
+
await reporter.recordSessionId("cron-123", "run-abc", "sess-early-1");
|
|
412
|
+
|
|
413
|
+
expect(state.captured).toHaveLength(1);
|
|
414
|
+
expect(state.captured[0].method).toBe("PATCH");
|
|
415
|
+
expect(state.captured[0].url).toContain(
|
|
416
|
+
`/agents/${AGENT_ID}/crons/cron-123/runs/run-abc`,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
420
|
+
expect(Object.keys(body)).toEqual(["sessionId"]);
|
|
421
|
+
expect(body.sessionId).toBe("sess-early-1");
|
|
422
|
+
expect(body.completedAt).toBeUndefined();
|
|
423
|
+
expect(body.outcome).toBeUndefined();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("recordSessionId does nothing when runId is null", async () => {
|
|
427
|
+
const reporter = makeReporter();
|
|
428
|
+
await reporter.recordSessionId("cron-123", null, "sess-early-1");
|
|
429
|
+
|
|
430
|
+
expect(state.captured).toHaveLength(0);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("recordSessionId swallows network errors (does not throw)", async () => {
|
|
434
|
+
const reporter = new HttpCronRunReporter({
|
|
435
|
+
apiUrl: "http://localhost:19999", // nothing listening
|
|
436
|
+
agentId: AGENT_ID,
|
|
437
|
+
apiKey: API_KEY,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
await expect(
|
|
441
|
+
reporter.recordSessionId("cron-net-err", "run-1", "sess-early-1"),
|
|
442
|
+
).resolves.toBeUndefined();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("recordSessionId swallows HTTP error responses (does not throw)", async () => {
|
|
446
|
+
state.patchStatusToReturn = 500;
|
|
447
|
+
const reporter = makeReporter();
|
|
448
|
+
|
|
449
|
+
await expect(
|
|
450
|
+
reporter.recordSessionId("cron-http-err", "run-1", "sess-early-1"),
|
|
451
|
+
).resolves.toBeUndefined();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ─── skipRun ───────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
test("skipRun PATCHes to correct URL with skipped:true and skipReason", async () => {
|
|
457
|
+
const reporter = makeReporter();
|
|
458
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
459
|
+
|
|
460
|
+
await reporter.skipRun(
|
|
461
|
+
"cron-123",
|
|
462
|
+
"run-abc",
|
|
463
|
+
completedAt,
|
|
464
|
+
"preCheck:not-found",
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
expect(state.captured).toHaveLength(1);
|
|
468
|
+
expect(state.captured[0].method).toBe("PATCH");
|
|
469
|
+
expect(state.captured[0].url).toContain(
|
|
470
|
+
`/agents/${AGENT_ID}/crons/cron-123/runs/run-abc`,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
474
|
+
expect(body.skipped).toBe(true);
|
|
475
|
+
expect(body.skipReason).toBe("preCheck:not-found");
|
|
476
|
+
expect(body.completedAt).toBe(completedAt.toISOString());
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("skipRun sends error when provided", async () => {
|
|
480
|
+
const reporter = makeReporter();
|
|
481
|
+
|
|
482
|
+
await reporter.skipRun(
|
|
483
|
+
"cron-123",
|
|
484
|
+
"run-abc",
|
|
485
|
+
new Date(),
|
|
486
|
+
"preCheck:crash",
|
|
487
|
+
{ error: "stderr output" },
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
491
|
+
expect(body.error).toBe("stderr output");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("skipRun does nothing when runId is null", async () => {
|
|
495
|
+
const reporter = makeReporter();
|
|
496
|
+
await reporter.skipRun("cron-123", null, new Date(), "preCheck:not-found");
|
|
497
|
+
|
|
498
|
+
expect(state.captured).toHaveLength(0);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("skipRun swallows network errors (does not throw)", async () => {
|
|
502
|
+
const reporter = new HttpCronRunReporter({
|
|
503
|
+
apiUrl: "http://localhost:19999",
|
|
504
|
+
agentId: AGENT_ID,
|
|
505
|
+
apiKey: API_KEY,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await expect(
|
|
509
|
+
reporter.skipRun("cron-net-err", "run-1", new Date(), "preCheck:crash"),
|
|
510
|
+
).resolves.toBeUndefined();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("skipRun swallows HTTP error responses (does not throw)", async () => {
|
|
514
|
+
state.patchStatusToReturn = 500;
|
|
515
|
+
const reporter = makeReporter();
|
|
516
|
+
|
|
517
|
+
await expect(
|
|
518
|
+
reporter.skipRun("cron-http-err", "run-1", new Date(), "preCheck:crash"),
|
|
519
|
+
).resolves.toBeUndefined();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("skipRun sends token data when provided", async () => {
|
|
523
|
+
const reporter = makeReporter();
|
|
524
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
525
|
+
|
|
526
|
+
await reporter.skipRun(
|
|
527
|
+
"cron-123",
|
|
528
|
+
"run-abc",
|
|
529
|
+
completedAt,
|
|
530
|
+
"command:no-work",
|
|
531
|
+
{
|
|
532
|
+
inputTokens: 100,
|
|
533
|
+
outputTokens: 50,
|
|
534
|
+
cacheReadTokens: 20,
|
|
535
|
+
cacheCreationTokens: 10,
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
540
|
+
expect(body.skipped).toBe(true);
|
|
541
|
+
expect(body.skipReason).toBe("command:no-work");
|
|
542
|
+
expect(body.inputTokens).toBe(100);
|
|
543
|
+
expect(body.outputTokens).toBe(50);
|
|
544
|
+
expect(body.cacheReadTokens).toBe(20);
|
|
545
|
+
expect(body.cacheCreationTokens).toBe(10);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("skipRun sends modelBreakdown when provided", async () => {
|
|
549
|
+
const reporter = makeReporter();
|
|
550
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
551
|
+
|
|
552
|
+
await reporter.skipRun(
|
|
553
|
+
"cron-123",
|
|
554
|
+
"run-abc",
|
|
555
|
+
completedAt,
|
|
556
|
+
"command:no-work",
|
|
557
|
+
{
|
|
558
|
+
inputTokens: 300,
|
|
559
|
+
outputTokens: 150,
|
|
560
|
+
modelBreakdown: [
|
|
561
|
+
{
|
|
562
|
+
model: "claude-sonnet-4-5",
|
|
563
|
+
inputTokens: 200,
|
|
564
|
+
outputTokens: 100,
|
|
565
|
+
cacheReadTokens: 0,
|
|
566
|
+
cacheCreationTokens: 0,
|
|
567
|
+
costUsd: 0.002,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
model: "claude-haiku-4-5",
|
|
571
|
+
inputTokens: 100,
|
|
572
|
+
outputTokens: 50,
|
|
573
|
+
cacheReadTokens: 0,
|
|
574
|
+
cacheCreationTokens: 0,
|
|
575
|
+
costUsd: 0.001,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
582
|
+
expect(Array.isArray(body.modelBreakdown)).toBe(true);
|
|
583
|
+
const breakdown = body.modelBreakdown as Array<Record<string, unknown>>;
|
|
584
|
+
expect(breakdown).toHaveLength(2);
|
|
585
|
+
expect(breakdown[0].model).toBe("claude-sonnet-4-5");
|
|
586
|
+
expect(breakdown[0].inputTokens).toBe(200);
|
|
587
|
+
expect(breakdown[1].model).toBe("claude-haiku-4-5");
|
|
588
|
+
expect(breakdown[1].inputTokens).toBe(100);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("skipRun sends both error and token data together when provided", async () => {
|
|
592
|
+
const reporter = makeReporter();
|
|
593
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
594
|
+
|
|
595
|
+
await reporter.skipRun(
|
|
596
|
+
"cron-123",
|
|
597
|
+
"run-abc",
|
|
598
|
+
completedAt,
|
|
599
|
+
"command:no-work",
|
|
600
|
+
{ error: "partial run before skip", inputTokens: 42 },
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
604
|
+
expect(body.error).toBe("partial run before skip");
|
|
605
|
+
expect(body.inputTokens).toBe(42);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("skipRun sends sessionId when provided", async () => {
|
|
609
|
+
const reporter = makeReporter();
|
|
610
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
611
|
+
|
|
612
|
+
await reporter.skipRun(
|
|
613
|
+
"cron-123",
|
|
614
|
+
"run-abc",
|
|
615
|
+
completedAt,
|
|
616
|
+
"command:no-work",
|
|
617
|
+
{ sessionId: "session-xyz-789" },
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
621
|
+
expect(body.sessionId).toBe("session-xyz-789");
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test("skipRun omits sessionId when not provided", async () => {
|
|
625
|
+
const reporter = makeReporter();
|
|
626
|
+
const completedAt = new Date("2026-01-01T08:00:05.000Z");
|
|
627
|
+
|
|
628
|
+
await reporter.skipRun(
|
|
629
|
+
"cron-123",
|
|
630
|
+
"run-abc",
|
|
631
|
+
completedAt,
|
|
632
|
+
"command:no-work",
|
|
633
|
+
{ inputTokens: 50 },
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
637
|
+
expect(body.sessionId).toBeUndefined();
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
// ─── NoopCronRunReporter ──────────────────────────────────────────────────────
|
|
642
|
+
|
|
643
|
+
describe("NoopCronRunReporter", () => {
|
|
644
|
+
test("createRun returns null and does not throw", async () => {
|
|
645
|
+
const reporter = new NoopCronRunReporter();
|
|
646
|
+
const result = await reporter.createRun("any", new Date());
|
|
647
|
+
expect(result).toBeNull();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("completeRun resolves immediately and does not throw", async () => {
|
|
651
|
+
const reporter = new NoopCronRunReporter();
|
|
652
|
+
await expect(
|
|
653
|
+
reporter.completeRun("any", null, new Date(), "completed"),
|
|
654
|
+
).resolves.toBeUndefined();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("skipRun resolves immediately and does not throw", async () => {
|
|
658
|
+
const reporter = new NoopCronRunReporter();
|
|
659
|
+
await expect(
|
|
660
|
+
reporter.skipRun("any", null, new Date(), "preCheck:not-found"),
|
|
661
|
+
).resolves.toBeUndefined();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("skipRun accepts token fields in opts and does not throw", async () => {
|
|
665
|
+
const reporter = new NoopCronRunReporter();
|
|
666
|
+
await expect(
|
|
667
|
+
reporter.skipRun("any", null, new Date(), "command:no-work", {
|
|
668
|
+
inputTokens: 10,
|
|
669
|
+
outputTokens: 5,
|
|
670
|
+
cacheReadTokens: 1,
|
|
671
|
+
cacheCreationTokens: 1,
|
|
672
|
+
modelBreakdown: [
|
|
673
|
+
{
|
|
674
|
+
model: "claude-sonnet-4-5",
|
|
675
|
+
inputTokens: 10,
|
|
676
|
+
outputTokens: 5,
|
|
677
|
+
cacheReadTokens: 1,
|
|
678
|
+
cacheCreationTokens: 1,
|
|
679
|
+
costUsd: 0.001,
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
}),
|
|
683
|
+
).resolves.toBeUndefined();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("recordProgress resolves immediately and does not throw", async () => {
|
|
687
|
+
const reporter = new NoopCronRunReporter();
|
|
688
|
+
await expect(
|
|
689
|
+
reporter.recordProgress("any", null, [], new Date()),
|
|
690
|
+
).resolves.toBeUndefined();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("recordSessionId resolves immediately and does not throw", async () => {
|
|
694
|
+
const reporter = new NoopCronRunReporter();
|
|
695
|
+
await expect(
|
|
696
|
+
reporter.recordSessionId("any", null, "sess-1"),
|
|
697
|
+
).resolves.toBeUndefined();
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ─── run telemetry + context stamp fields (prompt-audit metrics) ─────────────
|
|
702
|
+
|
|
703
|
+
describe("HttpCronRunReporter — telemetry and context-stamp fields", () => {
|
|
704
|
+
// biome-ignore lint/suspicious/noExplicitAny: Server type param varies by bun version
|
|
705
|
+
let server: ReturnType<typeof Bun.serve<any>>;
|
|
706
|
+
let state: StubState;
|
|
707
|
+
let baseUrl: string;
|
|
708
|
+
|
|
709
|
+
beforeEach(() => {
|
|
710
|
+
state = { captured: [], postStatusToReturn: 201, patchStatusToReturn: 200 };
|
|
711
|
+
server = startStubServer(0, state);
|
|
712
|
+
baseUrl = `http://localhost:${server.port}`;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
afterEach(async () => {
|
|
716
|
+
await server.stop(true);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("completeRun forwards contextBaseline, turns, toolCalls, skillUsage, and stamp fields verbatim", async () => {
|
|
720
|
+
const reporter = new HttpCronRunReporter({
|
|
721
|
+
apiUrl: baseUrl,
|
|
722
|
+
agentId: "agent-abc",
|
|
723
|
+
apiKey: "k",
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await reporter.completeRun(
|
|
727
|
+
"cron-1",
|
|
728
|
+
"run-1",
|
|
729
|
+
new Date("2026-01-01T08:00:05.000Z"),
|
|
730
|
+
"completed",
|
|
731
|
+
{
|
|
732
|
+
contextBaseline: {
|
|
733
|
+
model: "claude-sonnet-4-6",
|
|
734
|
+
contextTokens: 79_188,
|
|
735
|
+
inputTokens: 2,
|
|
736
|
+
cacheCreationTokens: 41_415,
|
|
737
|
+
cacheReadTokens: 37_771,
|
|
738
|
+
},
|
|
739
|
+
turns: 12,
|
|
740
|
+
toolCalls: 7,
|
|
741
|
+
skillUsage: [
|
|
742
|
+
{
|
|
743
|
+
kind: "skill",
|
|
744
|
+
name: "shipwright:task-store",
|
|
745
|
+
invocations: 1,
|
|
746
|
+
turns: 3,
|
|
747
|
+
inputTokens: 12,
|
|
748
|
+
outputTokens: 100,
|
|
749
|
+
cacheReadTokens: 0,
|
|
750
|
+
cacheCreationTokens: 9_300,
|
|
751
|
+
invokeContextDelta: 103,
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
contextFingerprint: "abc123def456",
|
|
755
|
+
pluginVersion: "1.363.0",
|
|
756
|
+
claudeCodeVersion: "2.1.285",
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
761
|
+
expect(body.contextBaseline).toEqual({
|
|
762
|
+
model: "claude-sonnet-4-6",
|
|
763
|
+
contextTokens: 79_188,
|
|
764
|
+
inputTokens: 2,
|
|
765
|
+
cacheCreationTokens: 41_415,
|
|
766
|
+
cacheReadTokens: 37_771,
|
|
767
|
+
});
|
|
768
|
+
expect(body.turns).toBe(12);
|
|
769
|
+
expect(body.toolCalls).toBe(7);
|
|
770
|
+
expect(body.skillUsage).toEqual([
|
|
771
|
+
expect.objectContaining({ name: "shipwright:task-store", turns: 3 }),
|
|
772
|
+
]);
|
|
773
|
+
expect(body.contextFingerprint).toBe("abc123def456");
|
|
774
|
+
expect(body.pluginVersion).toBe("1.363.0");
|
|
775
|
+
expect(body.claudeCodeVersion).toBe("2.1.285");
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("skipRun omits every telemetry/stamp field when none is provided", async () => {
|
|
779
|
+
const reporter = new HttpCronRunReporter({
|
|
780
|
+
apiUrl: baseUrl,
|
|
781
|
+
agentId: "agent-abc",
|
|
782
|
+
apiKey: "k",
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
await reporter.skipRun(
|
|
786
|
+
"cron-1",
|
|
787
|
+
"run-1",
|
|
788
|
+
new Date("2026-01-01T08:00:05.000Z"),
|
|
789
|
+
"command:no-work",
|
|
790
|
+
{ sessionId: "s" },
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const body = state.captured[0].body as Record<string, unknown>;
|
|
794
|
+
for (const key of [
|
|
795
|
+
"contextBaseline",
|
|
796
|
+
"turns",
|
|
797
|
+
"toolCalls",
|
|
798
|
+
"skillUsage",
|
|
799
|
+
"contextFingerprint",
|
|
800
|
+
"pluginVersion",
|
|
801
|
+
"claudeCodeVersion",
|
|
802
|
+
]) {
|
|
803
|
+
expect(key in body).toBe(false);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
});
|