@pi-unipi/background-tasks 2.6.3 → 2.9.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/package.json +6 -5
- package/src/__tests__/anthropic-attribution.test.ts +0 -195
- package/src/__tests__/config.test.ts +0 -137
- package/src/__tests__/core.test.ts +0 -493
- package/src/__tests__/delegate-artifacts.test.ts +0 -528
- package/src/__tests__/delegate-budget.test.ts +0 -456
- package/src/__tests__/delegate-launch.test.ts +0 -676
- package/src/__tests__/delegate-result-package.test.ts +0 -350
- package/src/__tests__/delegate-seed.test.ts +0 -392
- package/src/__tests__/durable-fs.test.ts +0 -559
- package/src/__tests__/extension-api.test.ts +0 -579
- package/src/__tests__/fusion-artifacts.test.ts +0 -1039
- package/src/__tests__/fusion-budget.test.ts +0 -1356
- package/src/__tests__/fusion-claude-cache.test.ts +0 -320
- package/src/__tests__/fusion-config.test.ts +0 -335
- package/src/__tests__/fusion-context-prompts.test.ts +0 -670
- package/src/__tests__/fusion-evaluation.test.ts +0 -315
- package/src/__tests__/fusion-extraction-equivalence.test.ts +0 -58
- package/src/__tests__/fusion-golden-bytes.test.ts +0 -35
- package/src/__tests__/fusion-high-cardinality.test.ts +0 -192
- package/src/__tests__/fusion-model-selector.test.ts +0 -205
- package/src/__tests__/fusion-orchestrator.test.ts +0 -1194
- package/src/__tests__/fusion-rpc.test.ts +0 -369
- package/src/__tests__/fusion-sdk.test.ts +0 -1226
- package/src/__tests__/fusion-v5-core.test.ts +0 -219
- package/src/__tests__/fusion-validate-orchestrator.test.ts +0 -240
- package/src/__tests__/fusion-web-fetch.test.ts +0 -485
- package/src/__tests__/fusion-workflows.test.ts +0 -59
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +0 -109
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +0 -10
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +0 -21
- package/src/__tests__/helpers/fusion-canonical.ts +0 -140
- package/src/__tests__/helpers/fusion-fake-pi.ts +0 -279
- package/src/__tests__/helpers/fusion-golden-corpus.ts +0 -500
- package/src/__tests__/helpers/fusion-high-cardinality.ts +0 -140
- package/src/__tests__/helpers/normalize.ts +0 -22
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +0 -18
- package/src/__tests__/pi-launch.test.ts +0 -202
- package/src/__tests__/registry.test.ts +0 -1580
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +0 -130
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +0 -631
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +0 -403
- package/src/__tests__/scripted-provider/follow-up.test.ts +0 -448
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +0 -132
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +0 -310
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +0 -163
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +0 -179
- package/src/__tests__/scripted-provider/hook-probe-a.ts +0 -3
- package/src/__tests__/scripted-provider/hook-probe-b.ts +0 -3
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +0 -126
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +0 -153
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +0 -18
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +0 -477
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +0 -28
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +0 -49
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +0 -408
- package/src/__tests__/task-manager.test.ts +0 -479
- package/src/__tests__/windows-taskkill.test.ts +0 -161
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, it, type TestContext } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
-
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { join, resolve, isAbsolute } from 'node:path';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import type { AssistantMessage, UserMessage } from '@earendil-works/pi-ai';
|
|
8
|
-
import {
|
|
9
|
-
AuthStorage,
|
|
10
|
-
createAgentSession,
|
|
11
|
-
DefaultResourceLoader,
|
|
12
|
-
ModelRegistry,
|
|
13
|
-
SessionManager,
|
|
14
|
-
SettingsManager,
|
|
15
|
-
Theme,
|
|
16
|
-
type AgentSession,
|
|
17
|
-
type ExtensionUIContext,
|
|
18
|
-
type KeybindingsManager,
|
|
19
|
-
} from '@earendil-works/pi-coding-agent';
|
|
20
|
-
import type { Component, TUI } from '@earendil-works/pi-tui';
|
|
21
|
-
import { parseJsonText } from '../types.js';
|
|
22
|
-
import { resolvePiLaunch } from '../pi-launch.js';
|
|
23
|
-
import { CURRENT_MODEL_SELECTION, FUSION_MODEL_CONFIG_FILE } from '../fusion/config.js';
|
|
24
|
-
import {
|
|
25
|
-
FUSION_INPUT_SCHEMA_VERSION,
|
|
26
|
-
FUSION_LEGACY_RESULT_SCHEMA_VERSION,
|
|
27
|
-
FUSION_RESULT_SCHEMA_VERSION,
|
|
28
|
-
type FusionResultDetails,
|
|
29
|
-
} from '../fusion/types.js';
|
|
30
|
-
import { installFusionFakePi } from './helpers/fusion-fake-pi.js';
|
|
31
|
-
import { isolatedTestEnv, stripAnsi } from './helpers/normalize.js';
|
|
32
|
-
|
|
33
|
-
const backgroundTasksExtensionPath = resolve('src/index.ts');
|
|
34
|
-
const roots: string[] = [];
|
|
35
|
-
const savedEnv = new Map<string, string | undefined>();
|
|
36
|
-
const envKeys = [
|
|
37
|
-
'PATH',
|
|
38
|
-
'PI_CODING_AGENT_DIR',
|
|
39
|
-
'UNIPI_BG_FUSION_TEST_KEY',
|
|
40
|
-
'PI_SESSION_ID',
|
|
41
|
-
'PI_PROVIDER',
|
|
42
|
-
'PI_MODEL',
|
|
43
|
-
] as const;
|
|
44
|
-
|
|
45
|
-
type JsonRecord = Record<string, unknown>;
|
|
46
|
-
|
|
47
|
-
interface FusionFakeInvocation {
|
|
48
|
-
stage: string;
|
|
49
|
-
workflow: string;
|
|
50
|
-
provider: string;
|
|
51
|
-
model: string;
|
|
52
|
-
args: string[];
|
|
53
|
-
stdin: string;
|
|
54
|
-
env: {
|
|
55
|
-
PI_SESSION_ID: string | null;
|
|
56
|
-
PI_PROVIDER: string | null;
|
|
57
|
-
PI_MODEL: string | null;
|
|
58
|
-
PI_SKIP_VERSION_CHECK: string | null;
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface Harness {
|
|
63
|
-
session: AgentSession;
|
|
64
|
-
cwd: string;
|
|
65
|
-
root: string;
|
|
66
|
-
agentDir: string;
|
|
67
|
-
fakeLogPath: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface HarnessOptions {
|
|
71
|
-
fakeDelayMs?: number | undefined;
|
|
72
|
-
fakeFailStage?: 'candidate' | 'evaluation' | 'evaluation-repair' | 'merge' | undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function skipWin32FusionChildPathFixture(t: TestContext): boolean {
|
|
76
|
-
if (process.platform !== 'win32') return false;
|
|
77
|
-
t.skip(
|
|
78
|
-
'PATH-based fake Pi child interception is not applicable on win32 because production resolves the Pi package instead of PATH by design',
|
|
79
|
-
);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function rememberEnv(): void {
|
|
84
|
-
if (savedEnv.size > 0) return;
|
|
85
|
-
for (const key of envKeys) savedEnv.set(key, process.env[key]);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function restoreEnvValue(key: string, value: string | undefined): void {
|
|
89
|
-
if (value === undefined) {
|
|
90
|
-
Reflect.deleteProperty(process.env, key);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
process.env[key] = value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function restoreEnv(): void {
|
|
97
|
-
for (const key of envKeys) restoreEnvValue(key, savedEnv.get(key));
|
|
98
|
-
savedEnv.clear();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function isRecord(value: unknown): value is JsonRecord {
|
|
102
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function stringField(record: JsonRecord, key: string): string {
|
|
106
|
-
const value = record[key];
|
|
107
|
-
if (typeof value !== 'string') throw new Error(`${key} must be a string`);
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function stringArray(value: unknown): string[] {
|
|
112
|
-
assert.ok(Array.isArray(value), 'expected string array');
|
|
113
|
-
return value.map((entry) => {
|
|
114
|
-
if (typeof entry !== 'string') throw new Error('array entry must be a string');
|
|
115
|
-
return entry;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function parseInvocation(line: string): FusionFakeInvocation {
|
|
120
|
-
const parsed = parseJsonText(line);
|
|
121
|
-
assert.ok(isRecord(parsed), 'fake invocation must be an object');
|
|
122
|
-
const env = parsed['env'];
|
|
123
|
-
assert.ok(isRecord(env), 'fake invocation env must be an object');
|
|
124
|
-
return {
|
|
125
|
-
stage: stringField(parsed, 'stage'),
|
|
126
|
-
workflow: stringField(parsed, 'workflow'),
|
|
127
|
-
provider: stringField(parsed, 'provider'),
|
|
128
|
-
model: stringField(parsed, 'model'),
|
|
129
|
-
args: stringArray(parsed['args']),
|
|
130
|
-
stdin: stringField(parsed, 'stdin'),
|
|
131
|
-
env: {
|
|
132
|
-
PI_SESSION_ID: env['PI_SESSION_ID'] === null ? null : stringField(env, 'PI_SESSION_ID'),
|
|
133
|
-
PI_PROVIDER: env['PI_PROVIDER'] === null ? null : stringField(env, 'PI_PROVIDER'),
|
|
134
|
-
PI_MODEL: env['PI_MODEL'] === null ? null : stringField(env, 'PI_MODEL'),
|
|
135
|
-
PI_SKIP_VERSION_CHECK:
|
|
136
|
-
env['PI_SKIP_VERSION_CHECK'] === null ? null : stringField(env, 'PI_SKIP_VERSION_CHECK'),
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function invocations(path: string): Promise<FusionFakeInvocation[]> {
|
|
142
|
-
if (!existsSync(path)) return [];
|
|
143
|
-
const raw = await readFile(path, 'utf8');
|
|
144
|
-
return raw.trim() ? raw.trim().split('\n').map(parseInvocation) : [];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function waitForInvocationCount(path: string, minimum: number): Promise<void> {
|
|
148
|
-
const start = Date.now();
|
|
149
|
-
while (Date.now() - start < 5000) {
|
|
150
|
-
if ((await invocations(path)).length >= minimum) return;
|
|
151
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
152
|
-
}
|
|
153
|
-
throw new Error(`timed out waiting for ${String(minimum)} fusion child calls`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function isFusionResultDetails(value: unknown): value is FusionResultDetails {
|
|
157
|
-
return (
|
|
158
|
-
isRecord(value) &&
|
|
159
|
-
value['schema_version'] === FUSION_RESULT_SCHEMA_VERSION &&
|
|
160
|
-
typeof value['run_id'] === 'string'
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isFusionLaunchDetails(value: unknown): value is JsonRecord {
|
|
165
|
-
return (
|
|
166
|
-
isRecord(value) &&
|
|
167
|
-
value['schema_version'] === 'pi-background-tasks.fusion-launch.v1' &&
|
|
168
|
-
typeof value['run_id'] === 'string' &&
|
|
169
|
-
isRecord(value['task'])
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function waitForFusionTerminal(session: AgentSession, taskId?: string): Promise<JsonRecord> {
|
|
174
|
-
const start = Date.now();
|
|
175
|
-
while (Date.now() - start < 30_000) {
|
|
176
|
-
const found = customEntries(session, 'background-task-notification').find((entry) => {
|
|
177
|
-
const details = entry['details'];
|
|
178
|
-
return (
|
|
179
|
-
isRecord(details) &&
|
|
180
|
-
details['status'] !== 'running' &&
|
|
181
|
-
isRecord(details['fusion']) &&
|
|
182
|
-
(taskId === undefined || details['id'] === taskId)
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
if (found !== undefined) return found;
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
187
|
-
}
|
|
188
|
-
throw new Error(`timed out waiting for Fusion task ${taskId ?? '(unspecified)'}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function retrieveFusionLaunch(h: Harness, launch: { details?: unknown }) {
|
|
192
|
-
assert.ok(
|
|
193
|
-
isFusionLaunchDetails(launch.details),
|
|
194
|
-
'Fusion tool must return a background launch receipt',
|
|
195
|
-
);
|
|
196
|
-
const task = launch.details['task'];
|
|
197
|
-
assert.ok(isRecord(task));
|
|
198
|
-
const taskId = stringField(task, 'id');
|
|
199
|
-
await waitForFusionTerminal(h.session, taskId);
|
|
200
|
-
const resultTool = h.session.getToolDefinition('bg_result');
|
|
201
|
-
assert.ok(resultTool, 'bg_result should be registered');
|
|
202
|
-
const result = await resultTool.execute(
|
|
203
|
-
`result-${taskId}`,
|
|
204
|
-
{ taskId, delivery: 'inline' },
|
|
205
|
-
undefined,
|
|
206
|
-
undefined,
|
|
207
|
-
h.session.extensionRunner.createContext(),
|
|
208
|
-
);
|
|
209
|
-
return { result, launchDetails: launch.details };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function committedDetails(h: Harness, launchDetails: unknown): Promise<FusionResultDetails> {
|
|
213
|
-
assert.ok(isFusionLaunchDetails(launchDetails));
|
|
214
|
-
const artifactDir = stringField(launchDetails, 'artifact_dir');
|
|
215
|
-
// OUR artifact_dir is absolute (temp root); only join when relative.
|
|
216
|
-
const resultPath = isAbsolute(artifactDir) ? join(artifactDir, 'result.json') : join(h.cwd, artifactDir, 'result.json');
|
|
217
|
-
const committed = parseJsonText(await readFile(resultPath, 'utf8'));
|
|
218
|
-
assert.ok(isRecord(committed));
|
|
219
|
-
const details = committed['details'];
|
|
220
|
-
assert.ok(isFusionResultDetails(details));
|
|
221
|
-
return details;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function customEntries(session: AgentSession, customType: string): JsonRecord[] {
|
|
225
|
-
const entries: readonly unknown[] = session.sessionManager.getEntries();
|
|
226
|
-
return entries.filter((entry): entry is JsonRecord => {
|
|
227
|
-
return (
|
|
228
|
-
isRecord(entry) && entry['type'] === 'custom_message' && entry['customType'] === customType
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function escapeRegExp(value: string): string {
|
|
234
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function fusionArtifactText(cwd: string, artifactDir: string): Promise<Map<string, string>> {
|
|
238
|
-
const root = join(cwd, artifactDir);
|
|
239
|
-
const files = await readdir(root);
|
|
240
|
-
const out = new Map<string, string>();
|
|
241
|
-
for (const file of files) out.set(file, await readFile(join(root, file), 'utf8'));
|
|
242
|
-
return out;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function assertCleanArtifactBoundary(
|
|
246
|
-
cwd: string,
|
|
247
|
-
details: FusionResultDetails,
|
|
248
|
-
sentinel: string,
|
|
249
|
-
): Promise<void> {
|
|
250
|
-
const artifacts = await fusionArtifactText(cwd, details.artifact_dir);
|
|
251
|
-
assert.equal(
|
|
252
|
-
artifacts.has('context-omission-ledger.json'),
|
|
253
|
-
false,
|
|
254
|
-
'clean runs must not write a parent omission ledger',
|
|
255
|
-
);
|
|
256
|
-
assert.ok(artifacts.has('canonical-input.json'), 'clean runs must persist canonical input');
|
|
257
|
-
assert.ok(artifacts.has('budget-plan.json'), 'clean runs must persist budget plan');
|
|
258
|
-
assert.ok(artifacts.has('manifest.json'), 'clean runs must persist manifest');
|
|
259
|
-
assert.ok(artifacts.has('merged.md'), 'clean runs must persist merged output');
|
|
260
|
-
const manifest = parseJsonText(artifacts.get('manifest.json') ?? '');
|
|
261
|
-
assert.ok(isRecord(manifest), 'manifest must be an object');
|
|
262
|
-
assert.equal(manifest['run_id'], details.run_id);
|
|
263
|
-
assert.equal(manifest['workflow'], details.workflow);
|
|
264
|
-
const context = manifest['context'];
|
|
265
|
-
assert.ok(isRecord(context), 'manifest context must be an object');
|
|
266
|
-
assert.equal(context['kind'], 'clean_task');
|
|
267
|
-
assert.equal(context['ledger_artifact'], undefined);
|
|
268
|
-
const canonical = parseJsonText(artifacts.get('canonical-input.json') ?? '');
|
|
269
|
-
assert.ok(isRecord(canonical), 'canonical input must be an object');
|
|
270
|
-
assert.equal(canonical['schema_version'], FUSION_INPUT_SCHEMA_VERSION);
|
|
271
|
-
assert.equal(canonical['workflow'], details.workflow);
|
|
272
|
-
assert.equal(canonical['system_prompt'], undefined);
|
|
273
|
-
assert.equal(canonical['conversation_projection'], undefined);
|
|
274
|
-
assert.equal(canonical['context_omission_ledger'], undefined);
|
|
275
|
-
const canonicalContext = canonical['context'];
|
|
276
|
-
assert.ok(isRecord(canonicalContext), 'canonical context must be an object');
|
|
277
|
-
assert.equal(canonicalContext['kind'], 'clean_task');
|
|
278
|
-
const forbidden = new RegExp(escapeRegExp(sentinel));
|
|
279
|
-
for (const [file, text] of artifacts) {
|
|
280
|
-
assert.doesNotMatch(text, forbidden, `${file} must not contain parent sentinel`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function assistantMessageCount(session: AgentSession): number {
|
|
285
|
-
return session.sessionManager.getEntries().filter((entry) => {
|
|
286
|
-
return (
|
|
287
|
-
isRecord(entry) && isRecord(entry['message']) && entry['message']['role'] === 'assistant'
|
|
288
|
-
);
|
|
289
|
-
}).length;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async function harness(options: HarnessOptions = {}): Promise<Harness> {
|
|
293
|
-
rememberEnv();
|
|
294
|
-
const root = await mkdtemp(join(tmpdir(), 'pi-bg-fusion-sdk-'));
|
|
295
|
-
roots.push(root);
|
|
296
|
-
const cwd = join(root, 'project');
|
|
297
|
-
const agentDir = join(root, 'agent');
|
|
298
|
-
await mkdir(cwd, { recursive: true });
|
|
299
|
-
await mkdir(agentDir, { recursive: true });
|
|
300
|
-
process.env['PI_CODING_AGENT_DIR'] = agentDir;
|
|
301
|
-
process.env['UNIPI_BG_FUSION_TEST_KEY'] = 'test-key';
|
|
302
|
-
process.env['PI_SESSION_ID'] = 'stale-session';
|
|
303
|
-
process.env['PI_PROVIDER'] = 'stale-provider';
|
|
304
|
-
process.env['PI_MODEL'] = 'stale-model';
|
|
305
|
-
Object.assign(process.env, isolatedTestEnv);
|
|
306
|
-
const fake = await installFusionFakePi(root, {
|
|
307
|
-
mergedText: 'SDK fused answer.',
|
|
308
|
-
delayMs: options.fakeDelayMs,
|
|
309
|
-
failStage: options.fakeFailStage,
|
|
310
|
-
});
|
|
311
|
-
process.env['PATH'] = fake.env['PATH'];
|
|
312
|
-
const settingsManager = SettingsManager.inMemory({
|
|
313
|
-
defaultProvider: 'pi-bg-fusion',
|
|
314
|
-
defaultModel: 'current-model',
|
|
315
|
-
});
|
|
316
|
-
const loader = new DefaultResourceLoader({
|
|
317
|
-
cwd,
|
|
318
|
-
agentDir,
|
|
319
|
-
settingsManager,
|
|
320
|
-
additionalExtensionPaths: [backgroundTasksExtensionPath],
|
|
321
|
-
noExtensions: true,
|
|
322
|
-
noSkills: true,
|
|
323
|
-
noPromptTemplates: true,
|
|
324
|
-
noContextFiles: true,
|
|
325
|
-
noThemes: true,
|
|
326
|
-
});
|
|
327
|
-
await loader.reload();
|
|
328
|
-
const authStorage = AuthStorage.create(join(agentDir, 'auth.json'));
|
|
329
|
-
const modelRegistry = ModelRegistry.create(authStorage, undefined);
|
|
330
|
-
modelRegistry.registerProvider('pi-bg-fusion', {
|
|
331
|
-
name: 'Fusion SDK Provider',
|
|
332
|
-
baseUrl: 'https://example.invalid',
|
|
333
|
-
apiKey: 'UNIPI_BG_FUSION_TEST_KEY',
|
|
334
|
-
api: 'openai-responses',
|
|
335
|
-
models: [
|
|
336
|
-
{
|
|
337
|
-
id: 'current-model',
|
|
338
|
-
name: 'Current Model',
|
|
339
|
-
reasoning: true,
|
|
340
|
-
input: ['text'],
|
|
341
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
342
|
-
contextWindow: 272000,
|
|
343
|
-
maxTokens: 4096,
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
id: 'alt-model',
|
|
347
|
-
name: 'Alt Model',
|
|
348
|
-
reasoning: true,
|
|
349
|
-
input: ['text'],
|
|
350
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
351
|
-
contextWindow: 272000,
|
|
352
|
-
maxTokens: 4096,
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
});
|
|
356
|
-
const { session } = await createAgentSession({
|
|
357
|
-
cwd,
|
|
358
|
-
agentDir,
|
|
359
|
-
resourceLoader: loader,
|
|
360
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
361
|
-
settingsManager,
|
|
362
|
-
authStorage,
|
|
363
|
-
modelRegistry,
|
|
364
|
-
noTools: 'builtin',
|
|
365
|
-
});
|
|
366
|
-
const model = modelRegistry.find('pi-bg-fusion', 'current-model');
|
|
367
|
-
assert.ok(model, 'fusion model should exist');
|
|
368
|
-
await session.setModel(model);
|
|
369
|
-
assert.equal(session.model?.provider, 'pi-bg-fusion');
|
|
370
|
-
assert.equal(session.model?.id, 'current-model');
|
|
371
|
-
session.setThinkingLevel('low');
|
|
372
|
-
await session.extensionRunner.emit({ type: 'session_start', reason: 'startup' });
|
|
373
|
-
return { session, cwd, root, agentDir, fakeLogPath: fake.logPath };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function disposeHarness(h: Harness): Promise<void> {
|
|
377
|
-
try {
|
|
378
|
-
await h.session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
|
|
379
|
-
} finally {
|
|
380
|
-
h.session.dispose();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function command(session: AgentSession, name: string) {
|
|
385
|
-
const found = session.extensionRunner
|
|
386
|
-
.getRegisteredCommands()
|
|
387
|
-
.find((cmd) => cmd.invocationName === name);
|
|
388
|
-
assert.ok(found, `missing command ${name}`);
|
|
389
|
-
return found;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function commandContext(session: AgentSession, mode?: string) {
|
|
393
|
-
const ctx = session.extensionRunner.createCommandContext();
|
|
394
|
-
if (mode !== undefined) Object.defineProperty(ctx, 'mode', { value: mode, configurable: true });
|
|
395
|
-
return ctx;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function baseUi(session: AgentSession): ExtensionUIContext {
|
|
399
|
-
return session.extensionRunner.getUIContext();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function makeTheme(): Theme {
|
|
403
|
-
return new Theme(
|
|
404
|
-
{
|
|
405
|
-
accent: '#ffffff',
|
|
406
|
-
border: '#ffffff',
|
|
407
|
-
borderAccent: '#ffffff',
|
|
408
|
-
borderMuted: '#ffffff',
|
|
409
|
-
success: '#ffffff',
|
|
410
|
-
error: '#ffffff',
|
|
411
|
-
warning: '#ffffff',
|
|
412
|
-
muted: '#ffffff',
|
|
413
|
-
dim: '#ffffff',
|
|
414
|
-
text: '#ffffff',
|
|
415
|
-
thinkingText: '#ffffff',
|
|
416
|
-
userMessageText: '#ffffff',
|
|
417
|
-
customMessageText: '#ffffff',
|
|
418
|
-
customMessageLabel: '#ffffff',
|
|
419
|
-
toolTitle: '#ffffff',
|
|
420
|
-
toolOutput: '#ffffff',
|
|
421
|
-
mdHeading: '#ffffff',
|
|
422
|
-
mdLink: '#ffffff',
|
|
423
|
-
mdLinkUrl: '#ffffff',
|
|
424
|
-
mdCode: '#ffffff',
|
|
425
|
-
mdCodeBlock: '#ffffff',
|
|
426
|
-
mdCodeBlockBorder: '#ffffff',
|
|
427
|
-
mdQuote: '#ffffff',
|
|
428
|
-
mdQuoteBorder: '#ffffff',
|
|
429
|
-
mdHr: '#ffffff',
|
|
430
|
-
mdListBullet: '#ffffff',
|
|
431
|
-
toolDiffAdded: '#ffffff',
|
|
432
|
-
toolDiffRemoved: '#ffffff',
|
|
433
|
-
toolDiffContext: '#ffffff',
|
|
434
|
-
syntaxComment: '#ffffff',
|
|
435
|
-
syntaxKeyword: '#ffffff',
|
|
436
|
-
syntaxFunction: '#ffffff',
|
|
437
|
-
syntaxVariable: '#ffffff',
|
|
438
|
-
syntaxString: '#ffffff',
|
|
439
|
-
syntaxNumber: '#ffffff',
|
|
440
|
-
syntaxType: '#ffffff',
|
|
441
|
-
syntaxOperator: '#ffffff',
|
|
442
|
-
syntaxPunctuation: '#ffffff',
|
|
443
|
-
thinkingOff: '#ffffff',
|
|
444
|
-
thinkingMinimal: '#ffffff',
|
|
445
|
-
thinkingLow: '#ffffff',
|
|
446
|
-
thinkingMedium: '#ffffff',
|
|
447
|
-
thinkingHigh: '#ffffff',
|
|
448
|
-
thinkingXhigh: '#ffffff',
|
|
449
|
-
bashMode: '#ffffff',
|
|
450
|
-
},
|
|
451
|
-
{
|
|
452
|
-
selectedBg: '#000000',
|
|
453
|
-
userMessageBg: '#000000',
|
|
454
|
-
customMessageBg: '#000000',
|
|
455
|
-
toolPendingBg: '#000000',
|
|
456
|
-
toolSuccessBg: '#000000',
|
|
457
|
-
toolErrorBg: '#000000',
|
|
458
|
-
},
|
|
459
|
-
'truecolor',
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function fakeTui(): TUI {
|
|
464
|
-
return { requestRender: () => undefined } as TUI;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function fakeKeybindings(): KeybindingsManager {
|
|
468
|
-
return {} as KeybindingsManager;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
afterEach(async () => {
|
|
472
|
-
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
|
473
|
-
restoreEnv();
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
void describe('fusion SDK integration', { concurrency: false }, () => {
|
|
477
|
-
void it('builds a fake Pi package accepted by the win32 launch resolver', async () => {
|
|
478
|
-
rememberEnv();
|
|
479
|
-
const root = await mkdtemp(join(tmpdir(), 'pi-bg-fusion-launch-'));
|
|
480
|
-
roots.push(root);
|
|
481
|
-
const fake = await installFusionFakePi(root);
|
|
482
|
-
const launch = resolvePiLaunch({
|
|
483
|
-
platform: 'win32',
|
|
484
|
-
resolvePackageJson: fake.resolvePackageJson,
|
|
485
|
-
execPath: process.execPath,
|
|
486
|
-
});
|
|
487
|
-
assert.equal(launch.executable, process.execPath);
|
|
488
|
-
// The expected value must be resolved with the same function the resolver
|
|
489
|
-
// uses. On Windows the synchronous and promise-based realpath implementations
|
|
490
|
-
// disagree about 8.3 short names: one preserves a component such as RUNNER~1
|
|
491
|
-
// while the other expands it to its long form.
|
|
492
|
-
assert.deepEqual(launch.argvPrefix, [realpathSync(fake.packageCliPath)]);
|
|
493
|
-
assert.equal(launch.kind, 'package-node-cli');
|
|
494
|
-
assert.equal((await readFile(fake.packageCliPath, 'utf8')).startsWith('#!'), false);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
void it('registers real public surfaces and /fusion launches a tracked background run', async (t) => {
|
|
498
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
499
|
-
const h = await harness();
|
|
500
|
-
try {
|
|
501
|
-
const activeTools = h.session.getActiveToolNames();
|
|
502
|
-
assert.ok(!activeTools.includes('fusion_brainstorm'));
|
|
503
|
-
for (const name of [
|
|
504
|
-
'fusion_reason',
|
|
505
|
-
'fusion_investigate',
|
|
506
|
-
'fusion_research',
|
|
507
|
-
'fusion_validate',
|
|
508
|
-
]) {
|
|
509
|
-
assert.ok(activeTools.includes(name), `${name} should be active`);
|
|
510
|
-
}
|
|
511
|
-
const fusionTool = h.session.getToolDefinition('fusion_reason');
|
|
512
|
-
assert.ok(fusionTool);
|
|
513
|
-
assert.equal(Reflect.get(fusionTool.parameters, 'additionalProperties'), false);
|
|
514
|
-
assert.ok(fusionTool.prepareArguments);
|
|
515
|
-
assert.throws(
|
|
516
|
-
() => fusionTool.prepareArguments?.({ prompt: 'x', capability: 'reason' }),
|
|
517
|
-
/unsupported key\(s\): capability/,
|
|
518
|
-
);
|
|
519
|
-
assert.deepEqual(fusionTool.prepareArguments?.({ prompt: 'x' }), { prompt: 'x' });
|
|
520
|
-
const commandNames = h.session.extensionRunner
|
|
521
|
-
.getRegisteredCommands()
|
|
522
|
-
.map((cmd) => cmd.invocationName);
|
|
523
|
-
assert.ok(commandNames.includes('unipi:fusion'));
|
|
524
|
-
assert.ok(commandNames.includes('unipi:fusion-models'));
|
|
525
|
-
const renderer = h.session.extensionRunner.getMessageRenderer('fusion-result');
|
|
526
|
-
assert.ok(renderer, 'fusion result renderer should be registered');
|
|
527
|
-
const legacyDetails = {
|
|
528
|
-
schema_version: FUSION_LEGACY_RESULT_SCHEMA_VERSION,
|
|
529
|
-
run_id: 'brainstorm-' + '1'.repeat(32),
|
|
530
|
-
workflow: 'brainstorm',
|
|
531
|
-
source: 'tool',
|
|
532
|
-
status: 'completed',
|
|
533
|
-
artifact_dir: '.pi/fusion/historical/brainstorm-' + '1'.repeat(32),
|
|
534
|
-
models: {},
|
|
535
|
-
evaluator_attempts: 1,
|
|
536
|
-
usage: {
|
|
537
|
-
input: 0,
|
|
538
|
-
output: 0,
|
|
539
|
-
cacheRead: 0,
|
|
540
|
-
cacheWrite: 0,
|
|
541
|
-
totalTokens: 0,
|
|
542
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
const legacyRendered = renderer(
|
|
546
|
-
{
|
|
547
|
-
role: 'custom',
|
|
548
|
-
customType: 'fusion-result',
|
|
549
|
-
content: 'Historical fused answer.',
|
|
550
|
-
display: true,
|
|
551
|
-
details: legacyDetails,
|
|
552
|
-
timestamp: Date.now(),
|
|
553
|
-
},
|
|
554
|
-
{ expanded: false },
|
|
555
|
-
makeTheme(),
|
|
556
|
-
);
|
|
557
|
-
assert.ok(legacyRendered, 'historical v4 fusion result should still render');
|
|
558
|
-
assert.match(stripAnsi(legacyRendered.render(100).join('\n')), /Historical fused answer/);
|
|
559
|
-
assert.ok(!h.session.getActiveToolNames().includes('fusion_brainstorm'));
|
|
560
|
-
await command(h.session, 'unipi:fusion').handler(
|
|
561
|
-
' command prompt\nwith body ',
|
|
562
|
-
commandContext(h.session, 'print'),
|
|
563
|
-
);
|
|
564
|
-
await waitForInvocationCount(h.fakeLogPath, 5);
|
|
565
|
-
const terminal = await waitForFusionTerminal(h.session);
|
|
566
|
-
const calls = await invocations(h.fakeLogPath);
|
|
567
|
-
assert.equal(calls.length, 5);
|
|
568
|
-
assert.equal(calls.filter((call) => call.stage === 'candidate').length, 3);
|
|
569
|
-
assert.equal(calls.filter((call) => call.stage === 'evaluation').length, 1);
|
|
570
|
-
assert.equal(calls.filter((call) => call.stage === 'merge').length, 1);
|
|
571
|
-
const candidateCalls = calls.filter((call) => call.stage === 'candidate');
|
|
572
|
-
const adjudicationCalls = calls.filter((call) => call.stage !== 'candidate');
|
|
573
|
-
for (const call of [...candidateCalls, ...adjudicationCalls]) {
|
|
574
|
-
assert.ok(call.args.includes('--no-tools'), `${call.stage} must run with --no-tools`);
|
|
575
|
-
assert.equal(call.args.includes('--no-builtin-tools'), false);
|
|
576
|
-
}
|
|
577
|
-
for (const call of calls) {
|
|
578
|
-
for (const flag of [
|
|
579
|
-
'--mode',
|
|
580
|
-
'--no-session',
|
|
581
|
-
'--no-extensions',
|
|
582
|
-
'--no-skills',
|
|
583
|
-
'--no-prompt-templates',
|
|
584
|
-
'--no-themes',
|
|
585
|
-
'--no-context-files',
|
|
586
|
-
'--provider',
|
|
587
|
-
'--model',
|
|
588
|
-
'--thinking',
|
|
589
|
-
'--system-prompt',
|
|
590
|
-
]) {
|
|
591
|
-
assert.ok(call.args.includes(flag), flag);
|
|
592
|
-
}
|
|
593
|
-
assert.equal(call.provider, 'pi-bg-fusion');
|
|
594
|
-
assert.equal(call.model, 'current-model');
|
|
595
|
-
assert.equal(call.env.PI_SESSION_ID, null);
|
|
596
|
-
assert.equal(call.env.PI_PROVIDER, null);
|
|
597
|
-
assert.equal(call.env.PI_MODEL, null);
|
|
598
|
-
assert.equal(call.env.PI_SKIP_VERSION_CHECK, '1');
|
|
599
|
-
}
|
|
600
|
-
const firstInput = parseJsonText(calls[0]?.stdin ?? '');
|
|
601
|
-
assert.ok(isRecord(firstInput));
|
|
602
|
-
assert.equal(firstInput['schema_version'], FUSION_INPUT_SCHEMA_VERSION);
|
|
603
|
-
const commandRequest = firstInput['request'];
|
|
604
|
-
assert.ok(isRecord(commandRequest), 'canonical request must be an object');
|
|
605
|
-
assert.equal(commandRequest['text'], 'command prompt\nwith body');
|
|
606
|
-
assert.equal(commandRequest['source'], 'command');
|
|
607
|
-
assert.equal(commandRequest['authority'], 'directive_over_projected_conversation');
|
|
608
|
-
|
|
609
|
-
assert.equal(customEntries(h.session, 'fusion-request').length, 0);
|
|
610
|
-
assert.equal(customEntries(h.session, 'fusion-result').length, 0);
|
|
611
|
-
const terminalDetails = terminal['details'];
|
|
612
|
-
assert.ok(isRecord(terminalDetails));
|
|
613
|
-
assert.equal(terminalDetails['status'], 'completed');
|
|
614
|
-
const terminalFusion = terminalDetails['fusion'];
|
|
615
|
-
assert.ok(isRecord(terminalFusion));
|
|
616
|
-
assert.equal(terminalFusion['workflow'], 'reason');
|
|
617
|
-
assert.equal(assistantMessageCount(h.session), 0);
|
|
618
|
-
} finally {
|
|
619
|
-
await disposeHarness(h);
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
void it('runs fusion_reason as the host-level no-tools path', async (t) => {
|
|
624
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
625
|
-
const h = await harness();
|
|
626
|
-
try {
|
|
627
|
-
const tool = h.session.getToolDefinition('fusion_reason');
|
|
628
|
-
assert.ok(tool, 'fusion_reason tool should be registered');
|
|
629
|
-
const launch = await tool.execute(
|
|
630
|
-
'call-fusion-reason',
|
|
631
|
-
{ prompt: 'reason without repository inspection' },
|
|
632
|
-
undefined,
|
|
633
|
-
undefined,
|
|
634
|
-
h.session.extensionRunner.createContext(),
|
|
635
|
-
);
|
|
636
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
637
|
-
const { launchDetails } = await retrieveFusionLaunch(h, launch);
|
|
638
|
-
const details = await committedDetails(h, launchDetails);
|
|
639
|
-
const calls = await invocations(h.fakeLogPath);
|
|
640
|
-
assert.equal(calls.length, 5);
|
|
641
|
-
for (const call of calls) {
|
|
642
|
-
assert.ok(call.args.includes('--no-tools'));
|
|
643
|
-
assert.equal(call.args.includes('--no-builtin-tools'), false);
|
|
644
|
-
}
|
|
645
|
-
const artifactFiles = await readdir(join(h.cwd, details.artifact_dir));
|
|
646
|
-
assert.equal(
|
|
647
|
-
artifactFiles.some((name) => name.includes('.tool-calls.')),
|
|
648
|
-
false,
|
|
649
|
-
'reason-only children must not create tool-call audit logs',
|
|
650
|
-
);
|
|
651
|
-
} finally {
|
|
652
|
-
await disposeHarness(h);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
void it('keeps all clean Fusion workflows free of parent sentinels in prompts and artifacts', async (t) => {
|
|
657
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
658
|
-
const h = await harness();
|
|
659
|
-
try {
|
|
660
|
-
const sentinel = 'PARENT-CLEAN-LEAK-SENTINEL-7f0d';
|
|
661
|
-
h.session.sessionManager.appendMessage({
|
|
662
|
-
role: 'user',
|
|
663
|
-
content: `visible parent ${sentinel}`,
|
|
664
|
-
timestamp: Date.now(),
|
|
665
|
-
});
|
|
666
|
-
h.session.sessionManager.appendMessage({
|
|
667
|
-
role: 'assistant',
|
|
668
|
-
api: 'openai-responses',
|
|
669
|
-
provider: 'pi-bg-fusion',
|
|
670
|
-
model: 'current-model',
|
|
671
|
-
usage: {
|
|
672
|
-
input: 1,
|
|
673
|
-
output: 1,
|
|
674
|
-
cacheRead: 0,
|
|
675
|
-
cacheWrite: 0,
|
|
676
|
-
totalTokens: 2,
|
|
677
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
678
|
-
},
|
|
679
|
-
stopReason: 'toolUse',
|
|
680
|
-
content: [
|
|
681
|
-
{ type: 'thinking', thinking: `hidden parent ${sentinel}` },
|
|
682
|
-
{ type: 'text', text: `assistant parent ${sentinel}` },
|
|
683
|
-
{ type: 'toolCall', id: 'parent-call', name: 'read', arguments: { sentinel } },
|
|
684
|
-
],
|
|
685
|
-
timestamp: Date.now(),
|
|
686
|
-
});
|
|
687
|
-
h.session.sessionManager.appendMessage({
|
|
688
|
-
role: 'toolResult',
|
|
689
|
-
toolCallId: 'parent-call',
|
|
690
|
-
toolName: 'read',
|
|
691
|
-
content: [{ type: 'text', text: `tool result parent ${sentinel}` }],
|
|
692
|
-
details: { sentinel },
|
|
693
|
-
isError: false,
|
|
694
|
-
timestamp: Date.now(),
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
const cases = [
|
|
698
|
-
{
|
|
699
|
-
name: 'fusion_investigate',
|
|
700
|
-
workflow: 'investigate',
|
|
701
|
-
candidateTools: 'read,grep,find,ls',
|
|
702
|
-
params: {
|
|
703
|
-
objective: 'inspect clean workflow',
|
|
704
|
-
background: ['self-contained public facts only'],
|
|
705
|
-
deliverable: 'answer',
|
|
706
|
-
scope: ['README.md'],
|
|
707
|
-
},
|
|
708
|
-
},
|
|
709
|
-
{
|
|
710
|
-
name: 'fusion_research',
|
|
711
|
-
workflow: 'research',
|
|
712
|
-
candidateTools: 'read,grep,find,ls,fusion_web_fetch',
|
|
713
|
-
params: {
|
|
714
|
-
objective: 'research clean workflow',
|
|
715
|
-
background: ['self-contained public facts only'],
|
|
716
|
-
deliverable: 'answer with source policy',
|
|
717
|
-
sources: [{ url: 'https://example.com/docs#section', purpose: 'declared source' }],
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
name: 'fusion_validate',
|
|
722
|
-
workflow: 'validate',
|
|
723
|
-
candidateTools: 'read,grep,find,ls',
|
|
724
|
-
params: {
|
|
725
|
-
objective: 'validate clean workflow',
|
|
726
|
-
background: ['self-contained validation facts only'],
|
|
727
|
-
changeSummary: 'changed public Fusion workflow facade',
|
|
728
|
-
scope: ['README.md'],
|
|
729
|
-
acceptanceCriteria: ['no parent sentinel reaches children'],
|
|
730
|
-
verification: { status: 'not_run', reason: 'SDK sentinel proof only' },
|
|
731
|
-
},
|
|
732
|
-
},
|
|
733
|
-
] as const;
|
|
734
|
-
|
|
735
|
-
for (const item of cases) {
|
|
736
|
-
await writeFile(h.fakeLogPath, '', 'utf8');
|
|
737
|
-
const tool = h.session.getToolDefinition(item.name);
|
|
738
|
-
assert.ok(tool, `${item.name} should be registered`);
|
|
739
|
-
const launch = await tool.execute(
|
|
740
|
-
`call-${item.workflow}`,
|
|
741
|
-
item.params,
|
|
742
|
-
undefined,
|
|
743
|
-
undefined,
|
|
744
|
-
h.session.extensionRunner.createContext(),
|
|
745
|
-
);
|
|
746
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
747
|
-
const retrieved = await retrieveFusionLaunch(h, launch);
|
|
748
|
-
const details = await committedDetails(h, retrieved.launchDetails);
|
|
749
|
-
assert.equal(details.workflow, item.workflow);
|
|
750
|
-
assert.ok(details.run_id.startsWith(`${item.workflow}-`));
|
|
751
|
-
assert.deepEqual(details.context, {
|
|
752
|
-
kind: 'clean_task',
|
|
753
|
-
policy_id: 'fusion-clean-task-v1',
|
|
754
|
-
});
|
|
755
|
-
assert.deepEqual(details.tool_policy.evaluation_tools, []);
|
|
756
|
-
assert.deepEqual(details.tool_policy.merge_tools, []);
|
|
757
|
-
assert.deepEqual(details.tool_policy.candidate_tools, item.candidateTools.split(','));
|
|
758
|
-
await assertCleanArtifactBoundary(h.cwd, details, sentinel);
|
|
759
|
-
|
|
760
|
-
const calls = await invocations(h.fakeLogPath);
|
|
761
|
-
assert.equal(calls.length, 5, `${item.name} must make exactly five child calls`);
|
|
762
|
-
const candidateCalls = calls.filter((call) => call.stage === 'candidate');
|
|
763
|
-
const adjudicators = calls.filter((call) => call.stage !== 'candidate');
|
|
764
|
-
assert.equal(candidateCalls.length, 3);
|
|
765
|
-
assert.equal(adjudicators.length, 2);
|
|
766
|
-
for (const call of calls) {
|
|
767
|
-
assert.doesNotMatch(call.stdin, new RegExp(escapeRegExp(sentinel)));
|
|
768
|
-
assert.doesNotMatch(call.stdin, /conversation_projection|conversation_transcript/);
|
|
769
|
-
assert.doesNotMatch(call.stdin, /context-omission-ledger/);
|
|
770
|
-
}
|
|
771
|
-
for (const call of candidateCalls) {
|
|
772
|
-
assert.equal(call.args.includes('--no-tools'), false);
|
|
773
|
-
assert.ok(call.args.includes('--no-builtin-tools'));
|
|
774
|
-
assert.equal(call.args[call.args.indexOf('--tools') + 1], item.candidateTools);
|
|
775
|
-
}
|
|
776
|
-
for (const call of adjudicators) {
|
|
777
|
-
assert.ok(call.args.includes('--no-tools'));
|
|
778
|
-
assert.equal(call.args.includes('--tools'), false);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
} finally {
|
|
782
|
-
await disposeHarness(h);
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
void it('BUG-182 returns exact merged text with host-valid usage and excludes the active tool-call leaf', async (t) => {
|
|
787
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
788
|
-
const h = await harness();
|
|
789
|
-
try {
|
|
790
|
-
const user: UserMessage = {
|
|
791
|
-
role: 'user',
|
|
792
|
-
content: [
|
|
793
|
-
{ type: 'text', text: 'prior user context before image ' },
|
|
794
|
-
{ type: 'image', data: 'sdk-raw-image-base64', mimeType: 'image/png' },
|
|
795
|
-
{ type: 'text', text: ' after image context' },
|
|
796
|
-
],
|
|
797
|
-
timestamp: Date.now(),
|
|
798
|
-
};
|
|
799
|
-
h.session.sessionManager.appendMessage(user);
|
|
800
|
-
const assistant: AssistantMessage = {
|
|
801
|
-
role: 'assistant',
|
|
802
|
-
api: 'openai-responses',
|
|
803
|
-
provider: 'pi-bg-fusion',
|
|
804
|
-
model: 'current-model',
|
|
805
|
-
usage: {
|
|
806
|
-
input: 1,
|
|
807
|
-
output: 1,
|
|
808
|
-
cacheRead: 0,
|
|
809
|
-
cacheWrite: 0,
|
|
810
|
-
totalTokens: 2,
|
|
811
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
812
|
-
},
|
|
813
|
-
stopReason: 'toolUse',
|
|
814
|
-
content: [
|
|
815
|
-
{ type: 'text', text: 'partial assistant text that must be excluded' },
|
|
816
|
-
{
|
|
817
|
-
type: 'toolCall',
|
|
818
|
-
id: 'call-fusion',
|
|
819
|
-
name: 'fusion_reason',
|
|
820
|
-
arguments: { prompt: 'tool prompt' },
|
|
821
|
-
},
|
|
822
|
-
{ type: 'toolCall', id: 'call-sibling', name: 'bg_status', arguments: {} },
|
|
823
|
-
],
|
|
824
|
-
timestamp: Date.now(),
|
|
825
|
-
};
|
|
826
|
-
h.session.sessionManager.appendMessage(assistant);
|
|
827
|
-
const tool = h.session.getToolDefinition('fusion_reason');
|
|
828
|
-
assert.ok(tool, 'fusion_reason tool should be registered');
|
|
829
|
-
const updates: string[] = [];
|
|
830
|
-
const launch = await tool.execute(
|
|
831
|
-
'call-fusion',
|
|
832
|
-
{ prompt: 'tool prompt' },
|
|
833
|
-
undefined,
|
|
834
|
-
(partial) => {
|
|
835
|
-
const text = partial.content[0]?.type === 'text' ? partial.content[0].text : '';
|
|
836
|
-
updates.push(text);
|
|
837
|
-
},
|
|
838
|
-
h.session.extensionRunner.createContext(),
|
|
839
|
-
);
|
|
840
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
841
|
-
const retrieved = await retrieveFusionLaunch(h, launch);
|
|
842
|
-
const result = retrieved.result;
|
|
843
|
-
const resultText = result.content[0]?.type === 'text' ? result.content[0].text : '';
|
|
844
|
-
assert.match(resultText, /SDK fused answer\.$/);
|
|
845
|
-
const details = await committedDetails(h, retrieved.launchDetails);
|
|
846
|
-
assert.equal(details.workflow, 'reason');
|
|
847
|
-
const resultUsage = Reflect.get(result, 'usage');
|
|
848
|
-
assert.ok(isRecord(resultUsage));
|
|
849
|
-
assert.equal(resultUsage['totalTokens'], 115);
|
|
850
|
-
assert.equal(resultUsage['costTotal'], undefined);
|
|
851
|
-
const resultCost = resultUsage['cost'];
|
|
852
|
-
assert.ok(isRecord(resultCost), 'tool usage must carry Pi Usage.cost');
|
|
853
|
-
assert.deepEqual(resultCost, {
|
|
854
|
-
input: 0.005,
|
|
855
|
-
output: 0.01,
|
|
856
|
-
cacheRead: 0.015,
|
|
857
|
-
cacheWrite: 0.02,
|
|
858
|
-
total: 0.05,
|
|
859
|
-
});
|
|
860
|
-
const task = retrieved.launchDetails['task'];
|
|
861
|
-
assert.ok(isRecord(task));
|
|
862
|
-
const resultTool = h.session.getToolDefinition('bg_result');
|
|
863
|
-
assert.ok(resultTool);
|
|
864
|
-
const repeated = await resultTool.execute(
|
|
865
|
-
'repeat-result',
|
|
866
|
-
{ taskId: stringField(task, 'id'), delivery: 'inline' },
|
|
867
|
-
undefined,
|
|
868
|
-
undefined,
|
|
869
|
-
h.session.extensionRunner.createContext(),
|
|
870
|
-
);
|
|
871
|
-
assert.equal(
|
|
872
|
-
Reflect.get(repeated, 'usage'),
|
|
873
|
-
undefined,
|
|
874
|
-
'repeated retrieval must not double-count usage',
|
|
875
|
-
);
|
|
876
|
-
assert.ok(updates.every((update) => !/failed/.test(update)));
|
|
877
|
-
|
|
878
|
-
const calls = await invocations(h.fakeLogPath);
|
|
879
|
-
const candidate = calls.find((call) => call.stage === 'candidate');
|
|
880
|
-
assert.ok(candidate, 'candidate invocation should be logged');
|
|
881
|
-
assert.ok(candidate.args.includes('--no-tools'));
|
|
882
|
-
assert.equal(candidate.args.includes('--no-builtin-tools'), false);
|
|
883
|
-
const parsedInput = parseJsonText(candidate.stdin);
|
|
884
|
-
assert.ok(isRecord(parsedInput), 'canonical input should be an object');
|
|
885
|
-
const toolRequest = parsedInput['request'];
|
|
886
|
-
assert.ok(isRecord(toolRequest), 'canonical request must be an object');
|
|
887
|
-
assert.equal(toolRequest['text'], 'tool prompt');
|
|
888
|
-
assert.equal(toolRequest['authority'], 'explicit_text');
|
|
889
|
-
// The exact bytes sent to the child carry the projection, not a raw transcript.
|
|
890
|
-
assert.doesNotMatch(candidate.stdin, /conversation_transcript/);
|
|
891
|
-
assert.match(candidate.stdin, /conversation_projection/);
|
|
892
|
-
// Scope conversation assertions to the projection: the parent system prompt
|
|
893
|
-
// legitimately names package tools such as bg_status.
|
|
894
|
-
const projection = parsedInput['conversation_projection'];
|
|
895
|
-
assert.ok(isRecord(projection), 'projection must be an object');
|
|
896
|
-
const projectionText = JSON.stringify(projection);
|
|
897
|
-
assert.match(projectionText, /prior user context before image/);
|
|
898
|
-
assert.match(projectionText, /after image context/);
|
|
899
|
-
assert.match(projectionText, /\[Image omitted from fusion text transcript: image\/png\]/);
|
|
900
|
-
assert.doesNotMatch(projectionText, /partial assistant text/);
|
|
901
|
-
assert.doesNotMatch(projectionText, /call-sibling|bg_status/);
|
|
902
|
-
// Raw image bytes must not appear anywhere in the child prompt.
|
|
903
|
-
assert.doesNotMatch(candidate.stdin, /sdk-raw-image-base64/);
|
|
904
|
-
} finally {
|
|
905
|
-
await disposeHarness(h);
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
void it('hands cancellation ownership from the tool call to the managed task after launch', async (t) => {
|
|
910
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
911
|
-
const h = await harness({ fakeDelayMs: 100 });
|
|
912
|
-
try {
|
|
913
|
-
const tool = h.session.getToolDefinition('fusion_reason');
|
|
914
|
-
assert.ok(tool);
|
|
915
|
-
const callController = new AbortController();
|
|
916
|
-
const launch = await tool.execute(
|
|
917
|
-
'call-signal-handoff',
|
|
918
|
-
{ prompt: 'continue after parent tool signal closes' },
|
|
919
|
-
callController.signal,
|
|
920
|
-
undefined,
|
|
921
|
-
h.session.extensionRunner.createContext(),
|
|
922
|
-
);
|
|
923
|
-
callController.abort();
|
|
924
|
-
const retrieved = await retrieveFusionLaunch(h, launch);
|
|
925
|
-
const text =
|
|
926
|
-
retrieved.result.content[0]?.type === 'text' ? retrieved.result.content[0].text : '';
|
|
927
|
-
assert.match(text, /SDK fused answer/);
|
|
928
|
-
} finally {
|
|
929
|
-
await disposeHarness(h);
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
void it('reports tool failure stage, slot, attempt, and durable artifact directory', async (t) => {
|
|
934
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
935
|
-
const h = await harness({ fakeFailStage: 'candidate' });
|
|
936
|
-
try {
|
|
937
|
-
const tool = h.session.getToolDefinition('fusion_reason');
|
|
938
|
-
assert.ok(tool, 'fusion_reason tool should be registered');
|
|
939
|
-
const launch = await tool.execute(
|
|
940
|
-
'call-failure-diagnostics',
|
|
941
|
-
{ prompt: 'fail with coordinates' },
|
|
942
|
-
undefined,
|
|
943
|
-
undefined,
|
|
944
|
-
h.session.extensionRunner.createContext(),
|
|
945
|
-
);
|
|
946
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
947
|
-
const task = launch.details['task'];
|
|
948
|
-
assert.ok(isRecord(task));
|
|
949
|
-
const taskId = stringField(task, 'id');
|
|
950
|
-
await waitForFusionTerminal(h.session, taskId);
|
|
951
|
-
const resultTool = h.session.getToolDefinition('bg_result');
|
|
952
|
-
assert.ok(resultTool);
|
|
953
|
-
const terminal = await resultTool.execute(
|
|
954
|
-
'result-failure-diagnostics',
|
|
955
|
-
{ taskId, delivery: 'inline' },
|
|
956
|
-
undefined,
|
|
957
|
-
undefined,
|
|
958
|
-
h.session.extensionRunner.createContext(),
|
|
959
|
-
);
|
|
960
|
-
assert.ok(isRecord(terminal.details));
|
|
961
|
-
assert.equal(terminal.details['state'], 'failed');
|
|
962
|
-
assert.equal(terminal.details['delivery'], 'none');
|
|
963
|
-
assert.deepEqual(terminal.details['answer'], { present: false, reason: 'run_did_not_commit' });
|
|
964
|
-
assert.equal(terminal.details['summary_status'], 'verified');
|
|
965
|
-
assert.equal(terminal.details['answer_bytes'], undefined);
|
|
966
|
-
assert.equal(terminal.details['answer_sha256'], undefined);
|
|
967
|
-
assert.ok(isRecord(terminal.details['failure']));
|
|
968
|
-
assert.equal(terminal.details['failure']['stage'], 'candidate');
|
|
969
|
-
assert.equal('usage' in terminal, false);
|
|
970
|
-
for (const delivery of [undefined, 'artifact'] as const) {
|
|
971
|
-
const repeat = await resultTool.execute(
|
|
972
|
-
`result-failure-${delivery ?? 'default'}`,
|
|
973
|
-
delivery === undefined ? { taskId } : { taskId, delivery },
|
|
974
|
-
undefined,
|
|
975
|
-
undefined,
|
|
976
|
-
h.session.extensionRunner.createContext(),
|
|
977
|
-
);
|
|
978
|
-
assert.ok(isRecord(repeat.details));
|
|
979
|
-
assert.equal(repeat.details['state'], 'failed');
|
|
980
|
-
assert.equal(repeat.details['delivery'], 'none');
|
|
981
|
-
assert.deepEqual(repeat.details['answer'], {
|
|
982
|
-
present: false,
|
|
983
|
-
reason: 'run_did_not_commit',
|
|
984
|
-
});
|
|
985
|
-
assert.equal('usage' in repeat, false);
|
|
986
|
-
}
|
|
987
|
-
} finally {
|
|
988
|
-
await disposeHarness(h);
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
void it('cancels live fusion children on session shutdown', async (t) => {
|
|
993
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
994
|
-
const h = await harness({ fakeDelayMs: 10000 });
|
|
995
|
-
let disposed = false;
|
|
996
|
-
try {
|
|
997
|
-
const tool = h.session.getToolDefinition('fusion_reason');
|
|
998
|
-
assert.ok(tool, 'fusion_reason tool should be registered');
|
|
999
|
-
const launchStarted = Date.now();
|
|
1000
|
-
const launch = await tool.execute(
|
|
1001
|
-
'call-shutdown',
|
|
1002
|
-
{ prompt: 'shutdown prompt' },
|
|
1003
|
-
undefined,
|
|
1004
|
-
undefined,
|
|
1005
|
-
h.session.extensionRunner.createContext(),
|
|
1006
|
-
);
|
|
1007
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
1008
|
-
assert.ok(
|
|
1009
|
-
Date.now() - launchStarted < 2_000,
|
|
1010
|
-
'launch receipt must not wait for delayed Fusion children',
|
|
1011
|
-
);
|
|
1012
|
-
const task = launch.details['task'];
|
|
1013
|
-
assert.ok(isRecord(task));
|
|
1014
|
-
const taskId = stringField(task, 'id');
|
|
1015
|
-
await waitForInvocationCount(h.fakeLogPath, 3);
|
|
1016
|
-
await h.session.extensionRunner.emit({ type: 'session_shutdown', reason: 'reload' });
|
|
1017
|
-
const resultTool = h.session.getToolDefinition('bg_result');
|
|
1018
|
-
assert.ok(resultTool);
|
|
1019
|
-
const terminal = await resultTool.execute(
|
|
1020
|
-
'result-shutdown',
|
|
1021
|
-
{ taskId, delivery: 'artifact' },
|
|
1022
|
-
undefined,
|
|
1023
|
-
undefined,
|
|
1024
|
-
h.session.extensionRunner.createContext(),
|
|
1025
|
-
);
|
|
1026
|
-
assert.ok(isRecord(terminal.details));
|
|
1027
|
-
assert.equal(terminal.details['state'], 'cancelled');
|
|
1028
|
-
assert.equal(terminal.details['delivery'], 'none');
|
|
1029
|
-
assert.deepEqual(terminal.details['answer'], { present: false, reason: 'run_did_not_commit' });
|
|
1030
|
-
assert.equal(terminal.details['summary_status'], 'verified');
|
|
1031
|
-
assert.equal(terminal.details['usage_delivered'], undefined);
|
|
1032
|
-
assert.equal('usage' in terminal, false);
|
|
1033
|
-
for (const delivery of [undefined, 'inline'] as const) {
|
|
1034
|
-
const repeat = await resultTool.execute(
|
|
1035
|
-
`result-shutdown-${delivery ?? 'default'}`,
|
|
1036
|
-
delivery === undefined ? { taskId } : { taskId, delivery },
|
|
1037
|
-
undefined,
|
|
1038
|
-
undefined,
|
|
1039
|
-
h.session.extensionRunner.createContext(),
|
|
1040
|
-
);
|
|
1041
|
-
assert.ok(isRecord(repeat.details));
|
|
1042
|
-
assert.equal(repeat.details['state'], 'cancelled');
|
|
1043
|
-
assert.equal(repeat.details['delivery'], 'none');
|
|
1044
|
-
assert.deepEqual(repeat.details['answer'], {
|
|
1045
|
-
present: false,
|
|
1046
|
-
reason: 'run_did_not_commit',
|
|
1047
|
-
});
|
|
1048
|
-
assert.equal('usage' in repeat, false);
|
|
1049
|
-
}
|
|
1050
|
-
h.session.dispose();
|
|
1051
|
-
disposed = true;
|
|
1052
|
-
} finally {
|
|
1053
|
-
if (!disposed) await disposeHarness(h);
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
void it('rejects /fusion-models without UI instead of using the no-op notifier', async () => {
|
|
1058
|
-
const h = await harness();
|
|
1059
|
-
try {
|
|
1060
|
-
await assert.rejects(
|
|
1061
|
-
command(h.session, 'unipi:fusion-models').handler('', commandContext(h.session)),
|
|
1062
|
-
/requires Pi TUI mode/,
|
|
1063
|
-
);
|
|
1064
|
-
assert.equal((await invocations(h.fakeLogPath)).length, 0);
|
|
1065
|
-
} finally {
|
|
1066
|
-
await disposeHarness(h);
|
|
1067
|
-
}
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
void it('supports no-argument editor flow, editor cancellation, selector save, and invalid config without child calls', async (t) => {
|
|
1071
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
1072
|
-
const h = await harness();
|
|
1073
|
-
try {
|
|
1074
|
-
const originalUi = baseUi(h.session);
|
|
1075
|
-
h.session.extensionRunner.setUIContext({
|
|
1076
|
-
...originalUi,
|
|
1077
|
-
editor: () => Promise.resolve(' editor prompt '),
|
|
1078
|
-
});
|
|
1079
|
-
await command(h.session, 'unipi:fusion').handler('', commandContext(h.session, 'print'));
|
|
1080
|
-
await waitForInvocationCount(h.fakeLogPath, 5);
|
|
1081
|
-
await waitForFusionTerminal(h.session);
|
|
1082
|
-
assert.equal((await invocations(h.fakeLogPath)).length, 5);
|
|
1083
|
-
|
|
1084
|
-
await writeFile(h.fakeLogPath, '', 'utf8');
|
|
1085
|
-
h.session.extensionRunner.setUIContext({
|
|
1086
|
-
...baseUi(h.session),
|
|
1087
|
-
editor: () => Promise.resolve(undefined),
|
|
1088
|
-
});
|
|
1089
|
-
await command(h.session, 'unipi:fusion').handler('', commandContext(h.session, 'print'));
|
|
1090
|
-
assert.equal((await invocations(h.fakeLogPath)).length, 0);
|
|
1091
|
-
|
|
1092
|
-
const selectorCustom: ExtensionUIContext['custom'] = (factory) => {
|
|
1093
|
-
return new Promise((resolvePromise, reject) => {
|
|
1094
|
-
Promise.resolve(factory(fakeTui(), makeTheme(), fakeKeybindings(), resolvePromise))
|
|
1095
|
-
.then((component: Component & { dispose?(): void }) => {
|
|
1096
|
-
component.handleInput?.('\r');
|
|
1097
|
-
component.handleInput?.('a');
|
|
1098
|
-
component.handleInput?.('l');
|
|
1099
|
-
component.handleInput?.('t');
|
|
1100
|
-
component.handleInput?.('\r');
|
|
1101
|
-
component.handleInput?.('\x1b[B');
|
|
1102
|
-
component.handleInput?.('\r');
|
|
1103
|
-
component.handleInput?.('a');
|
|
1104
|
-
component.handleInput?.('l');
|
|
1105
|
-
component.handleInput?.('t');
|
|
1106
|
-
component.handleInput?.('\r');
|
|
1107
|
-
component.handleInput?.('s');
|
|
1108
|
-
})
|
|
1109
|
-
.catch((error: unknown) => {
|
|
1110
|
-
reject(error);
|
|
1111
|
-
});
|
|
1112
|
-
});
|
|
1113
|
-
};
|
|
1114
|
-
// Pi 0.83 takes the run mode as a second setUIContext argument (default
|
|
1115
|
-
// "print"); /fusion-models is TUI-only, so the selector needs "tui".
|
|
1116
|
-
h.session.extensionRunner.setUIContext(
|
|
1117
|
-
{
|
|
1118
|
-
...baseUi(h.session),
|
|
1119
|
-
custom: selectorCustom,
|
|
1120
|
-
},
|
|
1121
|
-
'tui',
|
|
1122
|
-
);
|
|
1123
|
-
const selectorCtx = h.session.extensionRunner.createCommandContext();
|
|
1124
|
-
await command(h.session, 'unipi:fusion-models').handler('', selectorCtx);
|
|
1125
|
-
const savedConfigText = await readFile(join(h.agentDir, FUSION_MODEL_CONFIG_FILE), 'utf8');
|
|
1126
|
-
const savedConfig = parseJsonText(savedConfigText);
|
|
1127
|
-
assert.ok(isRecord(savedConfig));
|
|
1128
|
-
assert.deepEqual(savedConfig['candidates'], [
|
|
1129
|
-
'pi-bg-fusion/alt-model',
|
|
1130
|
-
'pi-bg-fusion/alt-model',
|
|
1131
|
-
CURRENT_MODEL_SELECTION,
|
|
1132
|
-
]);
|
|
1133
|
-
|
|
1134
|
-
await writeFile(h.fakeLogPath, '', 'utf8');
|
|
1135
|
-
await writeFile(join(h.agentDir, FUSION_MODEL_CONFIG_FILE), '{"bad":true}\n', 'utf8');
|
|
1136
|
-
const invalidTool = h.session.getToolDefinition('fusion_reason');
|
|
1137
|
-
assert.ok(invalidTool, 'fusion_reason tool should remain registered');
|
|
1138
|
-
await assert.rejects(
|
|
1139
|
-
() =>
|
|
1140
|
-
invalidTool.execute(
|
|
1141
|
-
'call-invalid',
|
|
1142
|
-
{ prompt: 'should not spawn' },
|
|
1143
|
-
undefined,
|
|
1144
|
-
undefined,
|
|
1145
|
-
h.session.extensionRunner.createContext(),
|
|
1146
|
-
),
|
|
1147
|
-
/schema_version|unknown key|missing key/,
|
|
1148
|
-
);
|
|
1149
|
-
assert.equal((await invocations(h.fakeLogPath)).length, 0);
|
|
1150
|
-
} finally {
|
|
1151
|
-
await disposeHarness(h);
|
|
1152
|
-
}
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
void it('runs fusion_validate end to end with inspect-only reviewers', async (t) => {
|
|
1156
|
-
if (skipWin32FusionChildPathFixture(t)) return;
|
|
1157
|
-
const h = await harness();
|
|
1158
|
-
try {
|
|
1159
|
-
assert.ok(h.session.getActiveToolNames().includes('fusion_validate'));
|
|
1160
|
-
const tool = h.session.getToolDefinition('fusion_validate');
|
|
1161
|
-
assert.ok(tool, 'fusion_validate should be registered at load');
|
|
1162
|
-
assert.equal(Reflect.get(tool.parameters, 'additionalProperties'), false);
|
|
1163
|
-
assert.throws(() => tool.prepareArguments?.({ prompt: 'x' }), /no longer accepts \{prompt\}/);
|
|
1164
|
-
assert.throws(
|
|
1165
|
-
() => tool.prepareArguments?.({ objective: 'x', extra: true }),
|
|
1166
|
-
/unsupported key\(s\): extra/,
|
|
1167
|
-
);
|
|
1168
|
-
|
|
1169
|
-
const launch = await tool.execute(
|
|
1170
|
-
'call-validate',
|
|
1171
|
-
{
|
|
1172
|
-
objective: 'validate the change',
|
|
1173
|
-
background: ['SDK integration test'],
|
|
1174
|
-
changeSummary: 'fusion public facade changed',
|
|
1175
|
-
scope: ['src/fusion-extension.ts'],
|
|
1176
|
-
acceptanceCriteria: ['validation runs with read-only reviewers'],
|
|
1177
|
-
verification: { status: 'provided', evidence: [{ check: 'sdk', outcome: 'running' }] },
|
|
1178
|
-
},
|
|
1179
|
-
undefined,
|
|
1180
|
-
undefined,
|
|
1181
|
-
h.session.extensionRunner.createContext(),
|
|
1182
|
-
);
|
|
1183
|
-
assert.ok(isFusionLaunchDetails(launch.details));
|
|
1184
|
-
const retrieved = await retrieveFusionLaunch(h, launch);
|
|
1185
|
-
const validateText =
|
|
1186
|
-
retrieved.result.content[0]?.type === 'text' ? retrieved.result.content[0].text : '';
|
|
1187
|
-
assert.match(validateText, /# Validation report/);
|
|
1188
|
-
assert.match(validateText, /Location: README\.md:1/);
|
|
1189
|
-
const details = await committedDetails(h, retrieved.launchDetails);
|
|
1190
|
-
assert.equal(details.workflow, 'validate');
|
|
1191
|
-
assert.ok(details.run_id.startsWith('validate-'));
|
|
1192
|
-
|
|
1193
|
-
const calls = await invocations(h.fakeLogPath);
|
|
1194
|
-
assert.equal(calls.length, 5, 'validate must make exactly five child calls');
|
|
1195
|
-
const candidates = calls.filter((call) => call.stage === 'candidate');
|
|
1196
|
-
const others = calls.filter((call) => call.stage !== 'candidate');
|
|
1197
|
-
assert.equal(candidates.length, 3);
|
|
1198
|
-
assert.equal(others.length, 2);
|
|
1199
|
-
|
|
1200
|
-
for (const call of candidates) {
|
|
1201
|
-
assert.equal(call.workflow, 'validate');
|
|
1202
|
-
// Reviewers get the read-only allowlist by argv, never --no-tools.
|
|
1203
|
-
assert.ok(!call.args.includes('--no-tools'));
|
|
1204
|
-
assert.ok(call.args.includes('--no-builtin-tools'));
|
|
1205
|
-
assert.equal(call.args[call.args.indexOf('--tools') + 1], 'read,grep,find,ls');
|
|
1206
|
-
const excluded = call.args[call.args.indexOf('--exclude-tools') + 1] ?? '';
|
|
1207
|
-
for (const forbidden of ['bash', 'edit', 'write', 'fusion_brainstorm', 'fusion_validate']) {
|
|
1208
|
-
assert.ok(excluded.includes(forbidden), `${forbidden} must be denied`);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
// Evaluator and merger adjudicate the review and must stay reasoning-only.
|
|
1212
|
-
for (const call of others) {
|
|
1213
|
-
assert.ok(call.args.includes('--no-tools'), `${call.stage} must run with --no-tools`);
|
|
1214
|
-
assert.ok(!call.args.includes('--tools'));
|
|
1215
|
-
}
|
|
1216
|
-
// Validate is clean by construction: no parent projection/transcript enters child prompts.
|
|
1217
|
-
for (const call of calls) {
|
|
1218
|
-
assert.ok(!call.stdin.includes('conversation_projection'));
|
|
1219
|
-
assert.ok(!call.stdin.includes('conversation_transcript'));
|
|
1220
|
-
assert.ok(call.stdin.includes(FUSION_INPUT_SCHEMA_VERSION));
|
|
1221
|
-
}
|
|
1222
|
-
} finally {
|
|
1223
|
-
await disposeHarness(h);
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1226
|
-
});
|