deepdebug-local-agent 1.9.0 → 1.10.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.
@@ -0,0 +1,370 @@
1
+ // F3-10 (2), the supervisor in isolation (fake session, scripted runners, fake clock where it matters): admission (concurrency, queue, memory, RSS),
2
+ // idempotent job ids, budget clamp, cancel (running, queued, unknown, a runner that ignores it), deadline, event coalescing and backpressure,
3
+ // terminal frames that are never lost, result field fallback. The end-to-end run through the real session and the smoke runner is jobs-e2e.test.js.
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { createJobManager, validateRunner, jobErrorCodeOf } from "../../src/la-protocol-v2/jobs/manager.js";
7
+ import { resolveJobsConfig } from "../../src/la-protocol-v2/jobs/config.js";
8
+ import { eventPayload, resultPayload } from "../../src/la-protocol-v2/jobs/events.js";
9
+ import { ProtocolError } from "../../src/la-protocol-v2/errors.js";
10
+ import { LlmClientError } from "../../src/la-protocol-v2/llm/client.js";
11
+
12
+ const noop = () => {};
13
+ const ENV = { LOCAL_AGENT_DLP_MODE: "enforce" };
14
+ const uuid = () => crypto.randomUUID();
15
+ const tick = (ms = 5) => new Promise((r) => setTimeout(r, ms));
16
+ const until = async (cond, ms = 3000) => { const t0 = Date.now(); while (!cond()) { if (Date.now() - t0 > ms) throw new Error("until: timed out"); await tick(5); } };
17
+ const MIB = 1024 * 1024;
18
+
19
+ function fakeSession({ failWith = [] } = {}) {
20
+ const frames = [];
21
+ const failures = [...failWith];
22
+ return { frames, enqueue: async (type, payload, opts) => { if (failures.length) { const code = failures.shift(); if (code) throw new ProtocolError(code); } frames.push({ type, payload, workspaceId: opts?.workspaceId }); return { acked: Promise.resolve() }; },
23
+ of: (jobId, type) => frames.filter((f) => f.payload.jobId === jobId && (!type || f.type === type)) };
24
+ }
25
+
26
+ const params = (over = {}) => ({ jobId: uuid(), kind: "quality", repoRef: { provider: "github", org: "acme", repo: "app", branch: "main" },
27
+ policy: { operationMode: "assist", automationLevel: 2, ceiling: "assist" }, budget: { tokens: 50_000, seconds: 60 }, llm: { alias: "vh-claude-sonnet", keyRef: "ab".repeat(32) }, inputs: [], ...over });
28
+ const ctxOf = (p, workspaceId = "ws-1") => ({ type: "job.assign", params: p, workspaceId });
29
+
30
+ /** A runner whose jobs finish when the test says so. */
31
+ function gatedRunner(over = {}) {
32
+ const calls = { started: [], cancelled: [], signals: new Map() };
33
+ const gates = new Map();
34
+ return {
35
+ calls, gates, name: "gated", needsLlm: false,
36
+ start(job, ctx) {
37
+ calls.started.push(job.jobId);
38
+ calls.signals.set(job.jobId, ctx.signal);
39
+ return new Promise((resolve, reject) => {
40
+ gates.set(job.jobId, { resolve, reject });
41
+ ctx.signal.addEventListener("abort", () => { if (!over.ignoreAbort) reject(new Error("aborted")); }, { once: true });
42
+ });
43
+ },
44
+ cancel(jobId) { calls.cancelled.push(jobId); },
45
+ ...over,
46
+ };
47
+ }
48
+
49
+ function mgr({ runner = gatedRunner(), env = {}, session = fakeSession(), resolveWorkspace = (id) => (id === "ws-1" || id === "ws-2" ? { root: `/w/${id}` } : null), memoryUsage, onResult, llm = null, readFile = null, now } = {}) {
50
+ const config = resolveJobsConfig({ LOCAL_AGENT_JOBS: "true", LOCAL_AGENT_JOBS_EVENT_MIN_MS: "50", LOCAL_AGENT_JOBS_CANCEL_GRACE_MS: "100", LOCAL_AGENT_JOBS_TERMINAL_RETRY_MS: "10", ...env }, noop);
51
+ const m = createJobManager({ session, tenantId: "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f", resolveWorkspace, config, env: ENV, runner, llm, readFile, onResult, log: { warn: noop }, ...(memoryUsage ? { memoryUsage } : {}), ...(now ? { now } : {}) });
52
+ return { m, session, runner, config };
53
+ }
54
+
55
+ // ------------------------------------------------------------------------------------------------------ config
56
+
57
+ test("config: off by default (unset, empty, unknown -> off, unknown warns); numbers are clamped; an unknown runner is ignored with a warning", () => {
58
+ for (const v of [undefined, "", "false", "no", "nope"]) {
59
+ const w = [];
60
+ assert.deepEqual(resolveJobsConfig({ LOCAL_AGENT_JOBS: v }, (m) => w.push(m)), { enabled: false });
61
+ assert.equal(w.length, v === "nope" ? 1 : 0);
62
+ }
63
+ const w = [];
64
+ const c = resolveJobsConfig({ LOCAL_AGENT_JOBS: "true", LOCAL_AGENT_JOBS_RUNNER: "evil", LOCAL_AGENT_JOBS_MAX_CONCURRENT: "500", LOCAL_AGENT_JOBS_MAX_TOKENS: "-3",
65
+ LOCAL_AGENT_JOBS_MAX_SECONDS: "abc", LOCAL_AGENT_JOBS_MAX_RSS_BYTES: "10", LOCAL_AGENT_JOBS_MAX_QUEUED: "9999" }, (m) => w.push(m));
66
+ assert.equal(c.runner, null); assert.equal(w.length, 1);
67
+ assert.equal(c.maxConcurrent, 1); assert.equal(c.maxTokens, 2_000_000); assert.equal(c.maxSeconds, 3600); assert.equal(c.maxRssBytes, 0); assert.equal(c.maxQueued, 4);
68
+ assert.equal(resolveJobsConfig({ LOCAL_AGENT_JOBS: "on", LOCAL_AGENT_JOBS_RUNNER: "smoke", LOCAL_AGENT_JOBS_MAX_RSS_BYTES: String(512 * MIB) }, noop).maxRssBytes, 512 * MIB);
69
+ });
70
+
71
+ test("runner interface: needs name, start() and cancel(); kinds only fix|implement|quality; setRunner refuses anything else", () => {
72
+ assert.equal(validateRunner(gatedRunner()), true);
73
+ for (const bad of [null, {}, { name: "x", start() {} }, { name: "X Y", start() {}, cancel() {} }, { name: "x", start() {}, cancel() {}, kinds: ["deploy"] }]) assert.equal(validateRunner(bad), false);
74
+ const { m } = mgr();
75
+ assert.throws(() => m.setRunner({ name: "x" }), /not a runner/);
76
+ });
77
+
78
+ // ------------------------------------------------------------------------------------------------------ admission
79
+
80
+ test("admission: no runner / DLP not enforce / a kind the runner does not do -> rejected protocol_unsupported; unknown workspace -> unauthorized; nothing starts", () => {
81
+ const noRunner = mgr({ runner: null });
82
+ assert.deepEqual(noRunner.m.handleAssign(ctxOf(params())), { reject: "protocol_unsupported" });
83
+ const r = gatedRunner({ kinds: ["quality"] });
84
+ const a = mgr({ runner: r });
85
+ assert.deepEqual(a.m.handleAssign(ctxOf(params({ kind: "fix" }))), { reject: "protocol_unsupported" });
86
+ assert.deepEqual(a.m.handleAssign(ctxOf(params(), "ws-nope")), { reject: "unauthorized" });
87
+ assert.deepEqual(a.m.handleAssign(ctxOf(params(), "")), { reject: "unauthorized" });
88
+ const cfg = resolveJobsConfig({ LOCAL_AGENT_JOBS: "true" }, noop);
89
+ const env = { LOCAL_AGENT_DLP_MODE: "enforce" };
90
+ const live = createJobManager({ session: fakeSession(), tenantId: "t", resolveWorkspace: () => ({ root: "/w" }), config: cfg, env, runner: gatedRunner(), log: { warn: noop } });
91
+ assert.deepEqual(live.handleAssign(ctxOf(params())), { accept: true });
92
+ env.LOCAL_AGENT_DLP_MODE = "warn"; // read per request
93
+ assert.deepEqual(live.handleAssign(ctxOf(params())), { reject: "protocol_unsupported" });
94
+ assert.equal(r.calls.started.length, 0);
95
+ live.cancelAll();
96
+ });
97
+
98
+ test("admission: concurrency 1 + queue 1: the 3rd job is rate_limited, the 2nd is queued (event `queued`) and runs when the 1st ends", async () => {
99
+ const { m, session, runner } = mgr({ env: { LOCAL_AGENT_JOBS_MAX_CONCURRENT: "1", LOCAL_AGENT_JOBS_MAX_QUEUED: "1" } });
100
+ const [p1, p2, p3] = [params(), params(), params()];
101
+ assert.deepEqual(m.handleAssign(ctxOf(p1)), { accept: true });
102
+ assert.deepEqual(m.handleAssign(ctxOf(p2)), { accept: true });
103
+ assert.deepEqual(m.handleAssign(ctxOf(p3)), { reject: "rate_limited" });
104
+ await until(() => runner.calls.started.length === 1);
105
+ assert.deepEqual(runner.calls.started, [p1.jobId], "only one runs");
106
+ assert.equal(session.of(p2.jobId, "job.event")[0].payload.state, "queued");
107
+ assert.equal(m.running, 1);
108
+ runner.gates.get(p1.jobId).resolve({ outcome: "no_change" });
109
+ await until(() => runner.calls.started.length === 2);
110
+ assert.deepEqual(runner.calls.started, [p1.jobId, p2.jobId]);
111
+ runner.gates.get(p2.jobId).resolve({ outcome: "no_change" });
112
+ await until(() => session.of(p2.jobId, "job.result").length === 1);
113
+ assert.equal(m.activeCount, 0); assert.equal(m.reservedBytes, 0, "memory reservation released");
114
+ });
115
+
116
+ test("admission: memory reservation (inline bytes + per-job) above the limit and RSS above the brake -> rate_limited; released when the job ends", async () => {
117
+ const a = mgr({ env: { LOCAL_AGENT_JOBS_MAX_CONCURRENT: "8", LOCAL_AGENT_JOBS_MEM_PER_JOB_BYTES: String(100 * MIB), LOCAL_AGENT_JOBS_MAX_MEMORY_BYTES: String(256 * MIB) } });
118
+ const [p1, p2, p3] = [params(), params(), params()];
119
+ assert.deepEqual(a.m.handleAssign(ctxOf(p1)), { accept: true });
120
+ assert.deepEqual(a.m.handleAssign(ctxOf(p2)), { accept: true });
121
+ assert.deepEqual(a.m.handleAssign(ctxOf(p3)), { reject: "rate_limited" }, "3 x 100 MiB > 256 MiB");
122
+ assert.ok(a.m.reservedBytes >= 200 * MIB);
123
+ await until(() => a.runner.calls.started.length === 2);
124
+ a.runner.gates.get(p1.jobId).resolve({ outcome: "no_change" });
125
+ await until(() => a.m.activeCount === 1);
126
+ assert.deepEqual(a.m.handleAssign(ctxOf(p3)), { accept: true }, "room again");
127
+ a.m.cancelAll();
128
+
129
+ let rss = 100 * MIB;
130
+ const b = mgr({ env: { LOCAL_AGENT_JOBS_MAX_RSS_BYTES: String(500 * MIB) }, memoryUsage: () => rss });
131
+ assert.deepEqual(b.m.handleAssign(ctxOf(params())), { accept: true });
132
+ rss = 800 * MIB;
133
+ assert.deepEqual(b.m.handleAssign(ctxOf(params())), { reject: "rate_limited" });
134
+ b.m.cancelAll();
135
+ });
136
+
137
+ test("idempotent: the same jobId again (running, queued or recently finished) is acked accepted and NOT started twice", async () => {
138
+ const { m, runner, session } = mgr({ env: { LOCAL_AGENT_JOBS_MAX_QUEUED: "2" } });
139
+ const p = params();
140
+ assert.deepEqual(m.handleAssign(ctxOf(p)), { accept: true });
141
+ assert.deepEqual(m.handleAssign(ctxOf(p)), { accept: true });
142
+ await until(() => runner.calls.started.length === 1);
143
+ assert.deepEqual(m.handleAssign(ctxOf(p)), { accept: true });
144
+ runner.gates.get(p.jobId).resolve({ outcome: "no_change" });
145
+ await until(() => session.of(p.jobId, "job.result").length === 1);
146
+ assert.deepEqual(m.handleAssign(ctxOf(p)), { accept: true }, "finished: remembered");
147
+ await tick(30);
148
+ assert.equal(runner.calls.started.length, 1);
149
+ assert.equal(m.stats.duplicate, 3);
150
+ });
151
+
152
+ test("budget: the requested budget is clamped to the local ceilings (and the runner sees the clamped one)", async () => {
153
+ let seen;
154
+ const runner = gatedRunner({ start(job) { seen = job.budget; return new Promise(() => {}); } });
155
+ const { m } = mgr({ runner, env: { LOCAL_AGENT_JOBS_MAX_TOKENS: "1000", LOCAL_AGENT_JOBS_MAX_SECONDS: "30" } });
156
+ m.handleAssign(ctxOf(params({ budget: { tokens: 9_999_999, seconds: 86_400 } })));
157
+ await until(() => seen);
158
+ assert.deepEqual(seen, { tokens: 1000, seconds: 30 });
159
+ assert.throws(() => { seen.tokens = 1; }, TypeError, "frozen");
160
+ m.cancelAll();
161
+ });
162
+
163
+ // ------------------------------------------------------------------------------------------------------ outcomes and cancel
164
+
165
+ test("outcomes: success -> job.result BEFORE the terminal job.event succeeded (+totals, errorCode none) (the control plane drops a result that follows the terminal event); failed/thrown -> failed with a code; the slot is freed", async () => {
166
+ const results = [];
167
+ const { m, session, runner } = mgr({ onResult: (r) => results.push(r) });
168
+ const ok = params(), bad = params(), boom = params();
169
+ for (const p of [ok, bad, boom]) m.handleAssign(ctxOf(p));
170
+ await until(() => runner.calls.started.length === 1);
171
+ runner.gates.get(ok.jobId).resolve({ outcome: "pr_opened", prUrl: "https://git.example/acme/app/pull/7", branch: "vh/fix-1", commitSha: "abcdef1", filesChangedCount: 2, diffSha256: "a".repeat(64), tests: { pass: 5, fail: 0, skip: 1 }, answer: "LOCAL ONLY" });
172
+ await until(() => session.of(ok.jobId, "job.result").length === 1 && session.of(ok.jobId, "job.event").length === 2);
173
+ const evs = session.frames.filter((f) => f.payload.jobId === ok.jobId);
174
+ assert.deepEqual(evs.map((f) => f.type), ["job.event", "job.result", "job.event"], "running, result, terminal event (result first)");
175
+ assert.equal(evs[2].payload.state, "succeeded"); assert.equal(evs[2].payload.errorCode, "none"); assert.equal(evs[2].payload.phase, "done");
176
+ assert.deepEqual(evs[1].payload, { jobId: ok.jobId, outcome: "pr_opened", prUrl: "https://git.example/acme/app/pull/7", branch: "vh/fix-1", commitSha: "abcdef1", filesChangedCount: 2, diffSha256: "a".repeat(64), tests: { pass: 5, fail: 0, skip: 1 } });
177
+ assert.ok(!JSON.stringify(session.frames).includes("LOCAL ONLY"), "the answer never goes on the wire");
178
+ assert.equal(results[0].answer, "LOCAL ONLY", "it goes to the local hook only");
179
+
180
+ await until(() => runner.calls.started.length === 2);
181
+ runner.gates.get(bad.jobId).resolve({ outcome: "failed", errorCode: "tests_failed" });
182
+ await until(() => session.of(bad.jobId, "job.result").length === 1);
183
+ assert.equal(session.of(bad.jobId, "job.event").at(-1).payload.errorCode, "tests_failed");
184
+ assert.equal(session.of(bad.jobId, "job.result")[0].payload.outcome, "failed");
185
+
186
+ await until(() => runner.calls.started.length === 3);
187
+ runner.gates.get(boom.jobId).reject(Object.assign(new Error("SECRET TEXT of an error"), { jobErrorCode: "build_failed" }));
188
+ await until(() => session.of(boom.jobId, "job.result").length === 1);
189
+ assert.equal(session.of(boom.jobId, "job.event").at(-1).payload.errorCode, "build_failed");
190
+ assert.ok(!JSON.stringify([session.frames, m.audit.tail(100)]).includes("SECRET TEXT"), "no error text anywhere");
191
+ });
192
+
193
+ test("error codes: LLM client codes map to job codes; anything else is `unknown` (the message is never read)", () => {
194
+ for (const [c, j] of [["budget_exceeded", "budget_exceeded"], ["timeout", "timeout"], ["cancelled", "cancelled"], ["dlp_blocked", "policy_denied"], ["dlp_denied", "policy_denied"], ["model_not_allowed", "policy_denied"], ["upstream_error", "llm_error"], ["no_key", "llm_error"], ["key_rejected", "llm_error"]]) {
195
+ assert.equal(jobErrorCodeOf(new LlmClientError(c)), j, c);
196
+ }
197
+ assert.equal(jobErrorCodeOf(new Error("x")), "unknown");
198
+ assert.equal(jobErrorCodeOf(Object.assign(new Error("x"), { jobErrorCode: "none" })), "unknown");
199
+ assert.equal(jobErrorCodeOf(Object.assign(new Error("x"), { jobErrorCode: "infra" })), "infra");
200
+ });
201
+
202
+ test("cancel: running job -> signal aborted, runner.cancel called, job.event cancelled then job.result cancelled; unknown / finished id = accepted no-op", async () => {
203
+ const { m, session, runner } = mgr();
204
+ const p = params();
205
+ m.handleAssign(ctxOf(p));
206
+ await until(() => runner.calls.started.length === 1);
207
+ assert.deepEqual(m.handleCancel({ params: { jobId: p.jobId, reason: "user" } }), { accept: true });
208
+ await until(() => session.of(p.jobId, "job.result").length === 1);
209
+ assert.equal(runner.calls.signals.get(p.jobId).aborted, true);
210
+ assert.deepEqual(runner.calls.cancelled, [p.jobId]);
211
+ assert.equal(session.of(p.jobId, "job.event").at(-1).payload.state, "cancelled");
212
+ assert.equal(session.of(p.jobId, "job.event").at(-1).payload.errorCode, "cancelled");
213
+ assert.equal(session.of(p.jobId, "job.result")[0].payload.outcome, "cancelled");
214
+ const n = session.frames.length;
215
+ assert.deepEqual(m.handleCancel({ params: { jobId: p.jobId, reason: "user" } }), { accept: true });
216
+ assert.deepEqual(m.handleCancel({ params: { jobId: uuid(), reason: "user" } }), { accept: true });
217
+ await tick(30);
218
+ assert.equal(session.frames.length, n, "no frame for an unknown or finished job (no oracle)");
219
+ });
220
+
221
+ test("cancel: a QUEUED job is cancelled at once (never started); a runner that ignores the abort is abandoned after the grace period", async () => {
222
+ const q = mgr({ env: { LOCAL_AGENT_JOBS_MAX_QUEUED: "2" } });
223
+ const [a, b] = [params(), params()];
224
+ q.m.handleAssign(ctxOf(a)); q.m.handleAssign(ctxOf(b));
225
+ await until(() => q.runner.calls.started.length === 1);
226
+ q.m.handleCancel({ params: { jobId: b.jobId, reason: "policy" } });
227
+ await until(() => q.session.of(b.jobId, "job.result").length === 1);
228
+ assert.equal(q.session.of(b.jobId, "job.result")[0].payload.outcome, "cancelled");
229
+ q.runner.gates.get(a.jobId).resolve({ outcome: "no_change" });
230
+ await tick(30);
231
+ assert.deepEqual(q.runner.calls.started, [a.jobId], "the cancelled one never ran");
232
+ q.m.cancelAll();
233
+
234
+ const stubborn = mgr({ runner: gatedRunner({ ignoreAbort: true }) });
235
+ const p = params();
236
+ stubborn.m.handleAssign(ctxOf(p));
237
+ await until(() => stubborn.runner.calls.started.length === 1);
238
+ stubborn.m.handleCancel({ params: { jobId: p.jobId, reason: "killswitch" } });
239
+ await until(() => stubborn.session.of(p.jobId, "job.result").length === 1, 2000);
240
+ assert.equal(stubborn.m.stats.abandoned, 1);
241
+ assert.equal(stubborn.m.running, 0, "the slot is free although the runner never returned");
242
+ stubborn.runner.gates.get(p.jobId).resolve({ outcome: "pr_opened" }); // a late result of an abandoned runner changes nothing
243
+ await tick(30);
244
+ assert.equal(stubborn.session.of(p.jobId, "job.result").length, 1);
245
+ });
246
+
247
+ test("deadline: the job's seconds budget aborts it -> failed timeout (job.event + job.result failed)", async () => {
248
+ const { m, session, runner } = mgr();
249
+ const p = params({ budget: { tokens: 1000, seconds: 1 } });
250
+ m.handleAssign(ctxOf(p));
251
+ await until(() => session.of(p.jobId, "job.result").length === 1, 4000);
252
+ assert.equal(session.of(p.jobId, "job.event").at(-1).payload.errorCode, "timeout");
253
+ assert.equal(session.of(p.jobId, "job.result")[0].payload.outcome, "failed");
254
+ assert.deepEqual(runner.calls.cancelled, [p.jobId]);
255
+ });
256
+
257
+ test("no runner needs the LLM but there is none / the key named by the job is not the held one -> failed llm_error (after the ack, a failure of the job, not of the channel)", async () => {
258
+ const needs = gatedRunner({ needsLlm: true });
259
+ const a = mgr({ runner: needs });
260
+ const p = params();
261
+ assert.deepEqual(a.m.handleAssign(ctxOf(p)), { accept: true });
262
+ await until(() => a.session.of(p.jobId, "job.result").length === 1);
263
+ assert.equal(a.session.of(p.jobId, "job.event").at(-1).payload.errorCode, "llm_error");
264
+ assert.equal(needs.calls.started.length, 0);
265
+ const holder = { held: true, matches: () => false };
266
+ const b = mgr({ runner: gatedRunner({ needsLlm: true }), llm: { client: {}, holder } });
267
+ const q = params();
268
+ b.m.handleAssign(ctxOf(q));
269
+ await until(() => b.session.of(q.jobId, "job.result").length === 1);
270
+ assert.equal(b.session.of(q.jobId, "job.event").at(-1).payload.errorCode, "llm_error");
271
+ });
272
+
273
+ // ------------------------------------------------------------------------------------------------------ events: coalescing and backpressure
274
+
275
+ test("progress is coalesced: a runner that reports 1000 times a flood-free number of frames goes out, the LAST state wins", async () => {
276
+ const runner = { name: "chatty", needsLlm: false, cancel: noop, async start(job, ctx) { for (let i = 1; i <= 1000; i++) ctx.progress({ phase: "edit", iteration: i % 1000, filesChanged: i }); await tick(120); return { outcome: "no_change" }; } };
277
+ const { m, session } = mgr({ runner });
278
+ const p = params();
279
+ m.handleAssign(ctxOf(p));
280
+ await until(() => session.of(p.jobId, "job.result").length === 1);
281
+ const progress = session.of(p.jobId, "job.event").filter((f) => f.payload.state === "running");
282
+ assert.ok(progress.length <= 6, `coalesced, got ${progress.length}`);
283
+ assert.equal(progress.at(-1).payload.filesChanged, 1000, "the last state is the one that is sent");
284
+ assert.equal(session.of(p.jobId, "job.event").at(-1).payload.state, "succeeded");
285
+ assert.ok(m.events.stats.progressSent >= 1);
286
+ });
287
+
288
+ test("backpressure: a full send queue drops PROGRESS frames (counted) but the terminal event and result are retried until they get through", async () => {
289
+ // the first calls fail with rate_limited: running (dropped), then the result (retried), then the terminal event (retried)
290
+ const session = fakeSession({ failWith: ["rate_limited", "rate_limited", "rate_limited", null, "not_ready", "rate_limited"] });
291
+ const { m, runner } = mgr({ session, env: { LOCAL_AGENT_JOBS_TERMINAL_RETRIES: "20" } });
292
+ const p = params();
293
+ m.handleAssign(ctxOf(p));
294
+ await until(() => runner.calls.started.length === 1);
295
+ await tick(20);
296
+ runner.gates.get(p.jobId).resolve({ outcome: "no_change" });
297
+ await until(() => session.of(p.jobId, "job.result").length === 1 && m.events.stats.terminalSent === 1, 3000);
298
+ assert.equal(m.events.stats.progressDropped >= 1, true);
299
+ assert.equal(session.of(p.jobId, "job.event").at(-1).payload.state, "succeeded");
300
+ assert.equal(m.events.stats.terminalSent, 1);
301
+ });
302
+
303
+ test("backpressure: after terminalRetries the terminal frame is given up (audited with a code), the supervisor stays healthy", async () => {
304
+ const session = fakeSession({ failWith: Array(50).fill("rate_limited") });
305
+ const { m, runner } = mgr({ session, env: { LOCAL_AGENT_JOBS_TERMINAL_RETRIES: "3" } });
306
+ const p = params();
307
+ m.handleAssign(ctxOf(p));
308
+ await until(() => runner.calls.started.length === 1);
309
+ runner.gates.get(p.jobId).resolve({ outcome: "no_change" });
310
+ await until(() => m.events.stats.terminalGivenUp >= 1, 3000);
311
+ assert.ok(m.audit.tail(100).some((a) => a.event === "job_event_given_up" && a.code === "rate_limited"));
312
+ assert.equal(m.running, 0);
313
+ assert.equal(m.handleAssign(ctxOf(params())).accept, true);
314
+ m.cancelAll();
315
+ });
316
+
317
+ test("result fallback: a runner field the schema refuses (a malformed PR url) does not lose the result: the outcome alone is sent", async () => {
318
+ const session = fakeSession();
319
+ const real = session.enqueue;
320
+ let refused = 0;
321
+ session.enqueue = async (type, payload, opts) => { // what the real session does when the closed schema refuses a field
322
+ if (type === "job.result" && payload.prUrl) { refused++; throw new ProtocolError("schema_violation"); }
323
+ return real(type, payload, opts);
324
+ };
325
+ const { m, runner } = mgr({ session });
326
+ const p = params();
327
+ m.handleAssign(ctxOf(p));
328
+ await until(() => runner.calls.started.length === 1);
329
+ runner.gates.get(p.jobId).resolve({ outcome: "pr_opened", prUrl: "http://not-https/x" });
330
+ await until(() => session.of(p.jobId, "job.result").length === 1);
331
+ assert.equal(refused, 1);
332
+ assert.deepEqual(session.of(p.jobId, "job.result")[0].payload, { jobId: p.jobId, outcome: "pr_opened" });
333
+ // the pure builders: unknown or invalid fields are dropped, never repaired
334
+ assert.deepEqual(resultPayload(p.jobId, "pr_opened", { prUrl: "https://x/y", extra: "ignored", tests: { pass: -1, fail: 0, skip: 0 } }), { jobId: p.jobId, outcome: "pr_opened", prUrl: "https://x/y" });
335
+ assert.deepEqual(resultPayload(p.jobId, "weird"), { jobId: p.jobId, outcome: "failed" });
336
+ assert.deepEqual(eventPayload({ jobId: p.jobId, state: "nonsense", phase: "x", iteration: 5000, filesRead: -1, tokensIn: 3, costUsd: 0.1234567891, errorCode: "boom" }), { jobId: p.jobId, state: "running", tokensIn: 3, costUsd: 0.123457 });
337
+ });
338
+
339
+ test("events carry only the whitelist: no field outside the contract can be put on a job.event / job.result by a runner", () => {
340
+ const e = eventPayload({ jobId: "j", state: "running", phase: "edit", iteration: 1, filesRead: 2, filesChanged: 3, tokensIn: 4, tokensOut: 5, costUsd: 0.5, durationMs: 6, errorCode: "none", prompt: "SECRET", path: "/etc/passwd", model: "m" });
341
+ assert.deepEqual(Object.keys(e).sort(), ["costUsd", "durationMs", "errorCode", "filesChanged", "filesRead", "iteration", "jobId", "phase", "state", "tokensIn", "tokensOut"]);
342
+ const r = resultPayload("j", "no_change", { answer: "SECRET", diff: "SECRET", stack: "SECRET" });
343
+ assert.deepEqual(r, { jobId: "j", outcome: "no_change" });
344
+ });
345
+
346
+ test("cancelAll (link close): every job aborted, runner.cancel called, memory released, nothing more is sent", async () => {
347
+ const { m, session, runner } = mgr({ env: { LOCAL_AGENT_JOBS_MAX_QUEUED: "2" } });
348
+ const ps = [params(), params(), params()];
349
+ for (const p of ps) m.handleAssign(ctxOf(p));
350
+ await until(() => runner.calls.started.length === 1);
351
+ const n = session.frames.length;
352
+ m.cancelAll();
353
+ assert.equal(m.activeCount, 0); assert.equal(m.reservedBytes, 0); assert.equal(m.running, 0);
354
+ assert.deepEqual(runner.calls.cancelled.sort(), ps.map((p) => p.jobId).sort());
355
+ assert.equal(runner.calls.signals.get(ps[0].jobId).aborted, true);
356
+ await tick(80);
357
+ assert.equal(session.frames.length, n);
358
+ assert.deepEqual(m.handleAssign(ctxOf(params())), { reject: "protocol_unsupported" }, "closed");
359
+ });
360
+
361
+ test("isolation: a job's context can only read through the job's own workspace (readFile gets the envelope's workspace, never one chosen by the runner)", async () => {
362
+ const asked = [];
363
+ const readFile = async (o) => { asked.push(o.workspaceId); return { ok: false, status: "not_found" }; };
364
+ const runner = { name: "r", needsLlm: false, cancel: noop, async start(job, ctx) { await ctx.readFile("x".repeat(43), { workspaceId: "ws-2" }); return { outcome: "no_change" }; } };
365
+ const { m, session } = mgr({ runner, readFile });
366
+ const p = params();
367
+ m.handleAssign(ctxOf(p, "ws-1"));
368
+ await until(() => session.of(p.jobId, "job.result").length === 1);
369
+ assert.deepEqual(asked, ["ws-1"]);
370
+ });