@pi-unipi/background-tasks 2.6.2 → 2.6.4

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 (59) hide show
  1. package/package.json +3 -2
  2. package/src/tools.ts +3 -1
  3. package/src/__tests__/anthropic-attribution.test.ts +0 -195
  4. package/src/__tests__/config.test.ts +0 -137
  5. package/src/__tests__/core.test.ts +0 -493
  6. package/src/__tests__/delegate-artifacts.test.ts +0 -528
  7. package/src/__tests__/delegate-budget.test.ts +0 -456
  8. package/src/__tests__/delegate-launch.test.ts +0 -676
  9. package/src/__tests__/delegate-result-package.test.ts +0 -350
  10. package/src/__tests__/delegate-seed.test.ts +0 -392
  11. package/src/__tests__/durable-fs.test.ts +0 -559
  12. package/src/__tests__/extension-api.test.ts +0 -579
  13. package/src/__tests__/fusion-artifacts.test.ts +0 -1039
  14. package/src/__tests__/fusion-budget.test.ts +0 -1356
  15. package/src/__tests__/fusion-claude-cache.test.ts +0 -320
  16. package/src/__tests__/fusion-config.test.ts +0 -335
  17. package/src/__tests__/fusion-context-prompts.test.ts +0 -670
  18. package/src/__tests__/fusion-evaluation.test.ts +0 -315
  19. package/src/__tests__/fusion-extraction-equivalence.test.ts +0 -58
  20. package/src/__tests__/fusion-golden-bytes.test.ts +0 -35
  21. package/src/__tests__/fusion-high-cardinality.test.ts +0 -192
  22. package/src/__tests__/fusion-model-selector.test.ts +0 -205
  23. package/src/__tests__/fusion-orchestrator.test.ts +0 -1194
  24. package/src/__tests__/fusion-rpc.test.ts +0 -369
  25. package/src/__tests__/fusion-sdk.test.ts +0 -1226
  26. package/src/__tests__/fusion-v5-core.test.ts +0 -219
  27. package/src/__tests__/fusion-validate-orchestrator.test.ts +0 -240
  28. package/src/__tests__/fusion-web-fetch.test.ts +0 -485
  29. package/src/__tests__/fusion-workflows.test.ts +0 -59
  30. package/src/__tests__/helpers/delegate-deterministic-seed.ts +0 -109
  31. package/src/__tests__/helpers/delegate-seed-subprocess.ts +0 -10
  32. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +0 -21
  33. package/src/__tests__/helpers/fusion-canonical.ts +0 -140
  34. package/src/__tests__/helpers/fusion-fake-pi.ts +0 -279
  35. package/src/__tests__/helpers/fusion-golden-corpus.ts +0 -500
  36. package/src/__tests__/helpers/fusion-high-cardinality.ts +0 -140
  37. package/src/__tests__/helpers/normalize.ts +0 -22
  38. package/src/__tests__/helpers/pi-hook-contract-evidence.json +0 -18
  39. package/src/__tests__/pi-launch.test.ts +0 -202
  40. package/src/__tests__/registry.test.ts +0 -1580
  41. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +0 -130
  42. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +0 -631
  43. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +0 -403
  44. package/src/__tests__/scripted-provider/follow-up.test.ts +0 -448
  45. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +0 -132
  46. package/src/__tests__/scripted-provider/fusion-reason.test.ts +0 -310
  47. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +0 -163
  48. package/src/__tests__/scripted-provider/hook-contract-provider.ts +0 -179
  49. package/src/__tests__/scripted-provider/hook-probe-a.ts +0 -3
  50. package/src/__tests__/scripted-provider/hook-probe-b.ts +0 -3
  51. package/src/__tests__/scripted-provider/hook-probe-extension.ts +0 -126
  52. package/src/__tests__/scripted-provider/output-recovery-provider.ts +0 -153
  53. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +0 -18
  54. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +0 -477
  55. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +0 -28
  56. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +0 -49
  57. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +0 -408
  58. package/src/__tests__/task-manager.test.ts +0 -479
  59. package/src/__tests__/windows-taskkill.test.ts +0 -161
@@ -1,631 +0,0 @@
1
- import { describe, it, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { existsSync } from 'node:fs';
4
- import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5
- import { tmpdir } from 'node:os';
6
- import { join, resolve } from 'node:path';
7
- import {
8
- AuthStorage,
9
- createAgentSession,
10
- DefaultResourceLoader,
11
- ModelRegistry,
12
- SessionManager,
13
- SettingsManager,
14
- } from '@earendil-works/pi-coding-agent';
15
- import { isolatedTestEnv } from '../helpers/normalize.js';
16
- import { buildDelegateSeed } from '../../delegate/seed.js';
17
- import { verifyDelegateResultPackage } from '../../delegate/result-package.js';
18
- import { DelegateError, type DelegateLimits, type DelegatePinnedRoute } from '../../delegate/types.js';
19
- import { sessionWith, userMessage } from '../helpers/fusion-canonical.js';
20
-
21
- /**
22
- * Delegate child-guard behaviour proven inside a real Pi agent loop.
23
- *
24
- * The guard extension is loaded into a real `AgentSession` driven by a
25
- * deterministic provider, so spill, budget refusal, route assertion, and the
26
- * atomic result commit are exercised through the actual hook dispatch path
27
- * rather than by calling handlers directly.
28
- */
29
- const guardExtensionPath = resolve('extensions/delegate-child.ts');
30
- const providerPath = resolve('tests/scripted-provider/delegate-guard-provider.ts');
31
-
32
- const roots: string[] = [];
33
-
34
- const ROUTE: DelegatePinnedRoute = {
35
- provider: 'pi-bg-delegate',
36
- model: 'delegate-model',
37
- qualified_id: 'pi-bg-delegate/delegate-model',
38
- context_window_tokens: 200_000,
39
- thinking_level: 'medium',
40
- origin: 'parent_current',
41
- };
42
-
43
- function limits(overrides: Partial<DelegateLimits> = {}): DelegateLimits {
44
- return {
45
- max_turns: 8,
46
- max_tool_calls: 20,
47
- timeout_seconds: 60,
48
- max_tool_result_bytes: 1024,
49
- max_total_tool_output_bytes: 8 * 1024 * 1024,
50
- max_answer_bytes: 4_194_304,
51
- allowed_input_tokens: 171_712,
52
- ...overrides,
53
- };
54
- }
55
-
56
- interface Harness {
57
- session: Awaited<ReturnType<typeof createAgentSession>>['session'];
58
- artifactDir: string;
59
- seedSha256: string;
60
- taskId: string;
61
- launchNonce: string;
62
- restore: () => void;
63
- }
64
-
65
- function restoreEnvValue(key: string, value: string | undefined): void {
66
- if (value === undefined) {
67
- Reflect.deleteProperty(process.env, key);
68
- return;
69
- }
70
- process.env[key] = value;
71
- }
72
-
73
- interface HarnessOptions {
74
- scenario: string;
75
- limits?: DelegateLimits | undefined;
76
- route?: DelegatePinnedRoute | undefined;
77
- directive?: string | undefined;
78
- }
79
-
80
- async function harness(options: HarnessOptions): Promise<Harness> {
81
- const root = await mkdtemp(join(tmpdir(), 'pi-bg-delegate-guard-'));
82
- roots.push(root);
83
- const cwd = join(root, 'project');
84
- const agentDir = join(root, 'agent');
85
- const artifactDir = join(root, 'artifacts');
86
- await mkdir(cwd, { recursive: true });
87
- await mkdir(agentDir, { recursive: true });
88
- await mkdir(join(artifactDir, 'spill'), { recursive: true });
89
-
90
- const taskId = 'd0123456789abcdef0123456789abcdef';
91
- const launchNonce = 'ffeeddccbbaa99887766554433221100';
92
- const route = options.route ?? ROUTE;
93
- const built = buildDelegateSeed(
94
- {
95
- cwd,
96
- sessionManager: sessionWith([userMessage('parent history line')]),
97
- getSystemPrompt: () => 'parent system prompt',
98
- },
99
- {
100
- taskId,
101
- launchNonce,
102
- toolCallId: 'delegate-call-1',
103
- directive: options.directive ?? 'investigate the thing',
104
- capability: 'inspect',
105
- extensionMode: 'isolated',
106
- route,
107
- limits: options.limits ?? limits(),
108
- },
109
- );
110
- const seedPath = join(artifactDir, 'seed.json');
111
- await writeFile(seedPath, built.serialized, 'utf8');
112
-
113
- const previous = {
114
- scenario: process.env['UNIPI_BG_DELEGATE_SCENARIO'],
115
- dir: process.env['UNIPI_BG_DELEGATE_ARTIFACT_DIR'],
116
- seed: process.env['UNIPI_BG_DELEGATE_SEED_PATH'],
117
- sha: process.env['UNIPI_BG_DELEGATE_SEED_SHA256'],
118
- task: process.env['UNIPI_BG_DELEGATE_TASK_ID'],
119
- nonce: process.env['UNIPI_BG_DELEGATE_LAUNCH_NONCE'],
120
- apiKey: process.env['UNIPI_BG_DELEGATE_API_KEY'],
121
- };
122
- Object.assign(process.env, isolatedTestEnv, {
123
- UNIPI_BG_DELEGATE_SCENARIO: options.scenario,
124
- UNIPI_BG_DELEGATE_ARTIFACT_DIR: artifactDir,
125
- UNIPI_BG_DELEGATE_SEED_PATH: seedPath,
126
- UNIPI_BG_DELEGATE_SEED_SHA256: built.sha256,
127
- UNIPI_BG_DELEGATE_TASK_ID: taskId,
128
- UNIPI_BG_DELEGATE_LAUNCH_NONCE: launchNonce,
129
- UNIPI_BG_DELEGATE_API_KEY: 'delegate-api-key',
130
- });
131
-
132
- const settingsManager = SettingsManager.inMemory({
133
- defaultProvider: 'pi-bg-delegate',
134
- defaultModel: 'delegate-model',
135
- });
136
- const loader = new DefaultResourceLoader({
137
- cwd,
138
- agentDir,
139
- settingsManager,
140
- additionalExtensionPaths: [providerPath, guardExtensionPath],
141
- noExtensions: true,
142
- noSkills: true,
143
- noPromptTemplates: true,
144
- noContextFiles: true,
145
- noThemes: true,
146
- });
147
- await loader.reload();
148
- const authStorage = AuthStorage.create(join(agentDir, 'auth.json'));
149
- const modelRegistry = ModelRegistry.create(authStorage);
150
- const { session } = await createAgentSession({
151
- cwd,
152
- agentDir,
153
- resourceLoader: loader,
154
- sessionManager: SessionManager.inMemory(cwd),
155
- settingsManager,
156
- authStorage,
157
- modelRegistry,
158
- noTools: 'builtin',
159
- });
160
- const model = modelRegistry.find('pi-bg-delegate', 'delegate-model');
161
- assert.ok(model, 'delegate guard provider model must be registered');
162
- await session.setModel(model);
163
-
164
- return {
165
- session,
166
- artifactDir,
167
- seedSha256: built.sha256,
168
- taskId,
169
- launchNonce,
170
- restore: () => {
171
- restoreEnvValue('UNIPI_BG_DELEGATE_SCENARIO', previous.scenario);
172
- restoreEnvValue('UNIPI_BG_DELEGATE_ARTIFACT_DIR', previous.dir);
173
- restoreEnvValue('UNIPI_BG_DELEGATE_SEED_PATH', previous.seed);
174
- restoreEnvValue('UNIPI_BG_DELEGATE_SEED_SHA256', previous.sha);
175
- restoreEnvValue('UNIPI_BG_DELEGATE_TASK_ID', previous.task);
176
- restoreEnvValue('UNIPI_BG_DELEGATE_LAUNCH_NONCE', previous.nonce);
177
- restoreEnvValue('UNIPI_BG_DELEGATE_API_KEY', previous.apiKey);
178
- },
179
- };
180
- }
181
-
182
- async function dispose(h: Harness): Promise<void> {
183
- try {
184
- await h.session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
185
- } finally {
186
- h.session.dispose();
187
- h.restore();
188
- }
189
- }
190
-
191
- async function readResult(h: Harness) {
192
- const raw = await readFile(join(h.artifactDir, 'result.json'), 'utf8');
193
- return verifyDelegateResultPackage(raw, {
194
- taskId: h.taskId,
195
- launchNonce: h.launchNonce,
196
- seedSha256: h.seedSha256,
197
- route: { provider: ROUTE.provider, model: ROUTE.model },
198
- });
199
- }
200
-
201
- async function readTerminal(h: Harness): Promise<Record<string, unknown>> {
202
- const raw = await readFile(join(h.artifactDir, 'child-terminal.json'), 'utf8');
203
- const parsed: unknown = JSON.parse(raw);
204
- assert.ok(typeof parsed === 'object' && parsed !== null);
205
- return parsed as Record<string, unknown>;
206
- }
207
-
208
- afterEach(async () => {
209
- await new Promise((resolve) => setTimeout(resolve, 100));
210
- for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
211
- });
212
-
213
- void describe('delegate child guard in a real agent loop', { concurrency: false }, () => {
214
- void it('commits exactly one verified result package for a clean run', { timeout: 20_000 }, async () => {
215
- const h = await harness({ scenario: 'plain-answer' });
216
- try {
217
- await h.session.prompt('do the work');
218
- await h.session.agent.waitForIdle();
219
- const verified = await readResult(h);
220
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
221
- assert.equal(verified.package.task_id, h.taskId);
222
- assert.equal(verified.package.seed_sha256, h.seedSha256);
223
- assert.equal(verified.package.route.provider, ROUTE.provider);
224
- assert.ok(verified.package.route_attestations.length > 0);
225
- assert.ok(!existsSync(join(h.artifactDir, 'child-terminal.json')));
226
- } finally {
227
- await dispose(h);
228
- }
229
- });
230
-
231
- void it(
232
- 'spills a 2 MB tool result to a hashed artifact and keeps the payload out of the transcript',
233
- { timeout: 30_000 },
234
- async () => {
235
- const h = await harness({ scenario: 'huge-tool-result' });
236
- try {
237
- await h.session.prompt('read the big thing');
238
- await h.session.agent.waitForIdle();
239
-
240
- const spills = await readdir(join(h.artifactDir, 'spill'));
241
- assert.equal(spills.length, 1, 'exactly one spill artifact must exist');
242
- const spillName = spills[0];
243
- assert.ok(spillName);
244
- const spilled = await readFile(join(h.artifactDir, 'spill', spillName));
245
- assert.equal(spilled.length, 2 * 1024 * 1024, 'the complete payload must be preserved');
246
-
247
- const verified = await readResult(h);
248
- assert.equal(verified.package.spilled_artifacts.length, 1);
249
- const receipt = verified.package.spilled_artifacts[0];
250
- assert.ok(receipt);
251
- assert.equal(receipt.byte_length, 2 * 1024 * 1024);
252
- assert.equal(receipt.tool_name, 'guard_probe');
253
-
254
- // The raw payload must never have entered the transcript.
255
- const entries: readonly unknown[] = h.session.sessionManager.getEntries();
256
- const serialized = JSON.stringify(entries);
257
- // Distinguish precisely: the marker legitimately appears in the
258
- // assistant's own tool-call arguments (the model wrote it). What must
259
- // never appear is the oversized payload inside a toolResult message.
260
- const toolResultText = entries
261
- .flatMap((entry) => {
262
- if (typeof entry !== 'object' || entry === null) return [];
263
- if (Reflect.get(entry, 'type') !== 'message') return [];
264
- const message = Reflect.get(entry, 'message');
265
- if (typeof message !== 'object' || message === null) return [];
266
- if (Reflect.get(message, 'role') !== 'toolResult') return [];
267
- const content = Reflect.get(message, 'content');
268
- if (!Array.isArray(content)) return [];
269
- return content.flatMap((part) =>
270
- typeof part === 'object' && part !== null && typeof Reflect.get(part, 'text') === 'string'
271
- ? [String(Reflect.get(part, 'text'))]
272
- : [],
273
- );
274
- })
275
- .join('\n');
276
- assert.ok(
277
- !toolResultText.includes('HUGEPAYLOAD'),
278
- 'the raw oversized payload must never reach the transcript as a tool result',
279
- );
280
- assert.ok(
281
- toolResultText.length < 4096,
282
- `the replaced tool result must be a compact receipt, not the payload (was ${String(toolResultText.length)} bytes)`,
283
- );
284
- assert.ok(
285
- serialized.includes('[delegate spill receipt]'),
286
- 'the transcript must carry an explicit receipt instead',
287
- );
288
- assert.ok(serialized.includes(receipt.sha256), 'the receipt must name the payload hash');
289
- assert.ok(
290
- serialized.includes('Nothing was truncated'),
291
- 'the receipt must state that nothing was truncated',
292
- );
293
- } finally {
294
- await dispose(h);
295
- }
296
- },
297
- );
298
-
299
- void it(
300
- 'preserves image-bearing tool results in a hashed structured spill',
301
- { timeout: 30_000 },
302
- async () => {
303
- const h = await harness({ scenario: 'image-tool-result' });
304
- try {
305
- await h.session.prompt('inspect the image result and answer');
306
- await h.session.agent.waitForIdle();
307
- const verified = await readResult(h);
308
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
309
- assert.equal(verified.package.spilled_artifacts.length, 1);
310
- const receipt = verified.package.spilled_artifacts[0];
311
- assert.ok(receipt);
312
- assert.equal(receipt.content_format, 'tool_result_content_json_v1');
313
- const raw = await readFile(join(h.artifactDir, receipt.artifact), 'utf8');
314
- const envelope = JSON.parse(raw) as Record<string, unknown>;
315
- assert.equal(
316
- envelope['schema_version'],
317
- 'pi-background-tasks.delegate-tool-result-content.v1',
318
- );
319
- const content = envelope['content'];
320
- assert.ok(Array.isArray(content));
321
- assert.equal(content.length, 2);
322
- const image = content[1];
323
- assert.ok(typeof image === 'object' && image !== null);
324
- assert.equal(Reflect.get(image, 'type'), 'image');
325
- assert.equal(Reflect.get(image, 'mimeType'), 'image/png');
326
- const data = Reflect.get(image, 'data');
327
- assert.equal(typeof data, 'string');
328
- assert.equal(
329
- Buffer.from(data, 'base64').toString('utf8'),
330
- 'IMAGE_SENTINEL_BYTES'.repeat(256),
331
- );
332
- } finally {
333
- await dispose(h);
334
- }
335
- },
336
- );
337
-
338
- void it(
339
- 'rejects malformed Unicode tool text instead of hashing substituted bytes',
340
- { timeout: 20_000 },
341
- async () => {
342
- const h = await harness({ scenario: 'invalid-unicode-tool-result' });
343
- try {
344
- await h.session.prompt('exercise malformed Unicode handling');
345
- await h.session.agent.waitForIdle();
346
- assert.ok(!existsSync(join(h.artifactDir, 'result.json')));
347
- const terminal = await readTerminal(h);
348
- assert.equal(terminal['code'], 'artifact_spill_failed');
349
- assert.match(String(terminal['message']), /without substitution/);
350
- } finally {
351
- await dispose(h);
352
- }
353
- },
354
- );
355
-
356
- void it(
357
- 'commits only final clean-stop text and excludes intermediate tool-use narration',
358
- { timeout: 20_000 },
359
- async () => {
360
- const h = await harness({ scenario: 'intermediate-narration' });
361
- try {
362
- await h.session.prompt('investigate before answering');
363
- await h.session.agent.waitForIdle();
364
- const verified = await readResult(h);
365
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
366
- assert.equal(verified.package.usage.status, 'observed');
367
- if (verified.package.usage.status === 'observed') {
368
- assert.equal(verified.package.usage.usage.input, 24);
369
- assert.equal(verified.package.usage.usage.output, 12);
370
- assert.equal(verified.package.usage.usage.totalTokens, 36);
371
- assert.equal(verified.package.usage.usage.cost.total, 0.006);
372
- }
373
- assert.ok(
374
- JSON.stringify(h.session.sessionManager.getEntries()).includes(
375
- 'INTERMEDIATE_NARRATION_MUST_NOT_BE_COMMITTED',
376
- ),
377
- 'intermediate narration should remain available in the transcript',
378
- );
379
- } finally {
380
- await dispose(h);
381
- }
382
- },
383
- );
384
-
385
- void it(
386
- 'does not repeat Fusion BUG-185 by aborting a live request from an advisory token estimate',
387
- { timeout: 20_000 },
388
- async () => {
389
- // A one-token advisory allowance guarantees the estimate is over. The
390
- // package must request graceful finalization and let Pi/provider capacity
391
- // handling proceed rather than self-reporting provider exhaustion.
392
- const h = await harness({
393
- scenario: 'plain-answer',
394
- limits: limits({ allowed_input_tokens: 1 }),
395
- });
396
- try {
397
- await h.session.prompt('this estimate is deliberately over its advisory allowance');
398
- await h.session.agent.waitForIdle();
399
- const verified = await readResult(h);
400
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
401
- assert.ok(!existsSync(join(h.artifactDir, 'child-terminal.json')));
402
- const runtime = JSON.parse(
403
- await readFile(join(h.artifactDir, 'runtime-budget.json'), 'utf8'),
404
- ) as Record<string, unknown>;
405
- assert.equal(runtime['finalization_requested'], true);
406
- } finally {
407
- await dispose(h);
408
- }
409
- },
410
- );
411
-
412
- void it(
413
- 'spills repeated sub-64-KiB results against route runway and still completes',
414
- { timeout: 30_000 },
415
- async () => {
416
- const h = await harness({
417
- scenario: 'subthreshold-growth',
418
- limits: limits({
419
- allowed_input_tokens: 120_000,
420
- max_turns: 12,
421
- max_tool_result_bytes: 64 * 1024,
422
- }),
423
- });
424
- try {
425
- await h.session.prompt('perform the repeated reads and then answer');
426
- await h.session.agent.waitForIdle();
427
- const verified = await readResult(h);
428
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
429
- assert.equal(verified.package.tool_calls, 8);
430
- assert.ok(
431
- verified.package.spilled_artifacts.length > 0,
432
- 'route pressure must spill results that are individually below 64 KiB',
433
- );
434
- assert.ok(
435
- verified.package.spilled_artifacts.every((receipt) => receipt.byte_length === 32 * 1024),
436
- );
437
- const runtime = JSON.parse(
438
- await readFile(join(h.artifactDir, 'runtime-budget.json'), 'utf8'),
439
- ) as Record<string, unknown>;
440
- assert.ok(Number(runtime['context_pressure_spill_bytes']) > 0);
441
- assert.ok(Number(runtime['retained_tool_result_bytes']) < 8 * 32 * 1024);
442
- } finally {
443
- await dispose(h);
444
- }
445
- },
446
- );
447
-
448
- void it(
449
- 'returns exactly the requested artifact range and refuses a short read',
450
- { timeout: 30_000 },
451
- async () => {
452
- const h = await harness({ scenario: 'spill-then-read' });
453
- try {
454
- await h.session.prompt('read a range from the spill');
455
- await h.session.agent.waitForIdle();
456
- const entries: readonly unknown[] = h.session.sessionManager.getEntries();
457
- const serialized = JSON.stringify(entries);
458
- assert.ok(
459
- serialized.includes('UkFOR0VNQVJLRVI='),
460
- 'the exact requested range must be returned as lossless base64',
461
- );
462
- assert.ok(
463
- serialized.includes('refused rather than silently shortened'),
464
- 'an over-long range read must fail loudly rather than return a short read',
465
- );
466
- } finally {
467
- await dispose(h);
468
- }
469
- },
470
- );
471
-
472
- void it(
473
- 'preserves a byte range that splits a multibyte UTF-8 sequence',
474
- { timeout: 30_000 },
475
- async () => {
476
- const h = await harness({
477
- scenario: 'split-utf8-range',
478
- limits: limits({ max_tool_result_bytes: 512 }),
479
- });
480
- try {
481
- await h.session.prompt('read exactly the second byte of the first UTF-8 sequence');
482
- await h.session.agent.waitForIdle();
483
- const serialized = JSON.stringify(h.session.sessionManager.getEntries());
484
- assert.ok(
485
- serialized.includes('qQ=='),
486
- 'the isolated 0xA9 continuation byte must survive as exact base64',
487
- );
488
- assert.ok(!serialized.includes('�'), 'the exact byte must never become U+FFFD');
489
- const verified = await readResult(h);
490
- assert.equal(verified.answer, 'DELEGATE_FINAL_ANSWER');
491
- } finally {
492
- await dispose(h);
493
- }
494
- },
495
- );
496
-
497
- void it(
498
- 'refuses to commit a success package when the observed route does not match the pin',
499
- { timeout: 20_000 },
500
- async () => {
501
- const h = await harness({ scenario: 'route-drift' });
502
- try {
503
- await h.session.prompt('answer from a drifted route');
504
- await h.session.agent.waitForIdle();
505
- assert.ok(
506
- !existsSync(join(h.artifactDir, 'result.json')),
507
- 'a route mismatch must never produce a committed answer',
508
- );
509
- const terminal = await readTerminal(h);
510
- assert.equal(terminal['code'], 'route_mismatch');
511
- } finally {
512
- await dispose(h);
513
- }
514
- },
515
- );
516
-
517
- void it('enforces the turn limit with a typed terminal record', { timeout: 30_000 }, async () => {
518
- const h = await harness({ scenario: 'many-turns', limits: limits({ max_turns: 2 }) });
519
- try {
520
- await h.session.prompt('loop a few times');
521
- await h.session.agent.waitForIdle();
522
- assert.ok(!existsSync(join(h.artifactDir, 'result.json')));
523
- const terminal = await readTerminal(h);
524
- assert.equal(terminal['code'], 'child_turn_limit');
525
- } finally {
526
- await dispose(h);
527
- }
528
- });
529
-
530
- void it(
531
- 'never commits an answer truncated by the output-token limit',
532
- { timeout: 20_000 },
533
- async () => {
534
- const h = await harness({ scenario: 'truncated-answer' });
535
- try {
536
- await h.session.prompt('produce a truncated answer');
537
- await h.session.agent.waitForIdle();
538
- // The bytes are intact and would hash correctly. They are still an
539
- // incomplete answer, so they must not be committed as a result.
540
- assert.ok(
541
- !existsSync(join(h.artifactDir, 'result.json')),
542
- 'a length-truncated response must never be committed as a complete answer',
543
- );
544
- const terminal = await readTerminal(h);
545
- assert.equal(terminal['code'], 'child_model_output_limit');
546
- assert.match(String(terminal['message']), /incomplete/);
547
- } finally {
548
- await dispose(h);
549
- }
550
- },
551
- );
552
-
553
- void it('enforces the declared answer capture contract without committing a prefix', { timeout: 20_000 }, async () => {
554
- const h = await harness({
555
- scenario: 'plain-answer',
556
- limits: limits({ max_answer_bytes: 8 }),
557
- });
558
- try {
559
- await h.session.prompt('produce an answer larger than the test capture contract');
560
- await h.session.agent.waitForIdle();
561
- assert.ok(!existsSync(join(h.artifactDir, 'result.json')));
562
- const terminal = await readTerminal(h);
563
- assert.equal(terminal['code'], 'child_capture_limit');
564
- assert.match(String(terminal['message']), /no prefix is committed/);
565
- } finally {
566
- await dispose(h);
567
- }
568
- });
569
-
570
- void it('never commits a whitespace-only answer', { timeout: 20_000 }, async () => {
571
- const h = await harness({ scenario: 'whitespace-answer' });
572
- try {
573
- await h.session.prompt('produce nothing useful');
574
- await h.session.agent.waitForIdle();
575
- assert.ok(!existsSync(join(h.artifactDir, 'result.json')));
576
- const terminal = await readTerminal(h);
577
- assert.equal(terminal['code'], 'child_result_invalid');
578
- } finally {
579
- await dispose(h);
580
- }
581
- });
582
-
583
- void it(
584
- 'turns zero advisory runway into a no-tool finalization rather than a false context failure',
585
- { timeout: 20_000 },
586
- async () => {
587
- const h = await harness({
588
- scenario: 'guard-throw',
589
- limits: limits({ allowed_input_tokens: 0 }),
590
- });
591
- try {
592
- await h.session.prompt('finalize without treating an estimate as provider truth');
593
- await h.session.agent.waitForIdle();
594
- const verified = await readResult(h);
595
- assert.equal(verified.answer, 'GUARD_THROW_SENTINEL_ANSWER');
596
- assert.ok(!existsSync(join(h.artifactDir, 'child-terminal.json')));
597
- } finally {
598
- await dispose(h);
599
- }
600
- },
601
- );
602
-
603
- void it(
604
- 'refuses a tampered seed at guard load rather than running on it',
605
- { timeout: 20_000 },
606
- async () => {
607
- const h = await harness({ scenario: 'plain-answer' });
608
- try {
609
- const seedPath = process.env['UNIPI_BG_DELEGATE_SEED_PATH'];
610
- assert.ok(seedPath);
611
- const original = await readFile(seedPath, 'utf8');
612
- const tampered = original.replace('parent history line', 'tampered history!!');
613
- assert.notEqual(tampered, original, 'the tamper must actually change the bytes');
614
- await writeFile(seedPath, tampered, 'utf8');
615
- const { verifyDelegateSeedBytes } = await import('../../delegate/seed.js');
616
- const onDisk = await readFile(seedPath, 'utf8');
617
- assert.throws(
618
- () =>
619
- verifyDelegateSeedBytes(onDisk, {
620
- sha256: h.seedSha256,
621
- taskId: h.taskId,
622
- launchNonce: h.launchNonce,
623
- }),
624
- (error: unknown) => error instanceof DelegateError && error.code === 'seed_hash_mismatch',
625
- );
626
- } finally {
627
- await dispose(h);
628
- }
629
- },
630
- );
631
- });