@pi-unipi/background-tasks 2.6.1
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/README.md +87 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/extensions/delegate-child.ts +1 -0
- package/extensions/fusion-child.ts +1 -0
- package/package.json +40 -0
- package/src/__tests__/anthropic-attribution.test.ts +195 -0
- package/src/__tests__/config.test.ts +137 -0
- package/src/__tests__/core.test.ts +493 -0
- package/src/__tests__/delegate-artifacts.test.ts +528 -0
- package/src/__tests__/delegate-budget.test.ts +456 -0
- package/src/__tests__/delegate-launch.test.ts +676 -0
- package/src/__tests__/delegate-result-package.test.ts +350 -0
- package/src/__tests__/delegate-seed.test.ts +392 -0
- package/src/__tests__/durable-fs.test.ts +559 -0
- package/src/__tests__/extension-api.test.ts +579 -0
- package/src/__tests__/fusion-artifacts.test.ts +1039 -0
- package/src/__tests__/fusion-budget.test.ts +1356 -0
- package/src/__tests__/fusion-claude-cache.test.ts +320 -0
- package/src/__tests__/fusion-config.test.ts +335 -0
- package/src/__tests__/fusion-context-prompts.test.ts +670 -0
- package/src/__tests__/fusion-evaluation.test.ts +315 -0
- package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
- package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
- package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
- package/src/__tests__/fusion-model-selector.test.ts +205 -0
- package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
- package/src/__tests__/fusion-rpc.test.ts +369 -0
- package/src/__tests__/fusion-sdk.test.ts +1226 -0
- package/src/__tests__/fusion-v5-core.test.ts +219 -0
- package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
- package/src/__tests__/fusion-web-fetch.test.ts +485 -0
- package/src/__tests__/fusion-workflows.test.ts +59 -0
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
- package/src/__tests__/helpers/fusion-canonical.ts +140 -0
- package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
- package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
- package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
- package/src/__tests__/helpers/normalize.ts +22 -0
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/pi-launch.test.ts +202 -0
- package/src/__tests__/registry.test.ts +1580 -0
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
- package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
- package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
- package/src/__tests__/task-manager.test.ts +479 -0
- package/src/__tests__/windows-taskkill.test.ts +161 -0
- package/src/anthropic-attribution-path.ts +21 -0
- package/src/anthropic-attribution.ts +1983 -0
- package/src/attested-pi-run.ts +612 -0
- package/src/child-process.ts +55 -0
- package/src/common.ts +8 -0
- package/src/config.ts +292 -0
- package/src/context-parent-snapshot.ts +142 -0
- package/src/context-token-budget.ts +903 -0
- package/src/context-visible-conversation-v2.ts +551 -0
- package/src/delegate/artifacts.ts +487 -0
- package/src/delegate/budget.ts +415 -0
- package/src/delegate/hook-contract-evidence.json +18 -0
- package/src/delegate/hook-contract.ts +154 -0
- package/src/delegate/launch.ts +497 -0
- package/src/delegate/result-package.ts +459 -0
- package/src/delegate/runner.ts +449 -0
- package/src/delegate/seed.ts +423 -0
- package/src/delegate/types.ts +323 -0
- package/src/delegate-child-extension.ts +978 -0
- package/src/delegate-extension.ts +806 -0
- package/src/durable-fs.ts +386 -0
- package/src/extension-api.ts +548 -0
- package/src/fixtures/delegate-context-incident.json +17 -0
- package/src/fixtures/fusion-golden-bytes.json +310 -0
- package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
- package/src/fusion/artifacts.ts +967 -0
- package/src/fusion/budget.ts +1162 -0
- package/src/fusion/child-protocol.ts +305 -0
- package/src/fusion/claude-cache.ts +207 -0
- package/src/fusion/clean-context.ts +91 -0
- package/src/fusion/config.ts +449 -0
- package/src/fusion/context.ts +265 -0
- package/src/fusion/evaluation.ts +800 -0
- package/src/fusion/orchestrator.ts +1288 -0
- package/src/fusion/output-contract.ts +34 -0
- package/src/fusion/pi-child.ts +2373 -0
- package/src/fusion/prompts.ts +345 -0
- package/src/fusion/result-package.ts +959 -0
- package/src/fusion/source-policy.ts +257 -0
- package/src/fusion/types.ts +1139 -0
- package/src/fusion/web-fetch.ts +1060 -0
- package/src/fusion/workflows.ts +184 -0
- package/src/fusion-child-extension.ts +1052 -0
- package/src/fusion-extension.ts +1293 -0
- package/src/index.ts +295 -0
- package/src/pi-launch.ts +225 -0
- package/src/registry.ts +2424 -0
- package/src/settings-overlay.ts +208 -0
- package/src/task-manager.ts +774 -0
- package/src/tools.ts +530 -0
- package/src/turndown.d.ts +15 -0
- package/src/types.ts +963 -0
- package/src/ui/fusion-model-selector.ts +322 -0
- package/src/windows-taskkill.ts +250 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import {
|
|
7
|
+
boundedRead,
|
|
8
|
+
compareSemver,
|
|
9
|
+
deriveCompletionDeliveryGuidance,
|
|
10
|
+
deriveTaskNameFromCommand,
|
|
11
|
+
formatAgentActivityLine,
|
|
12
|
+
parseAgentActivity,
|
|
13
|
+
formatCompactNumber,
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatModelSummary,
|
|
16
|
+
formatSnapshotList,
|
|
17
|
+
formatUpdateSegment,
|
|
18
|
+
isNewerVersion,
|
|
19
|
+
normalizeMaxBytes,
|
|
20
|
+
normalizeTaskName,
|
|
21
|
+
parseBgCommandArgs,
|
|
22
|
+
parseSemver,
|
|
23
|
+
sanitizePathSegment,
|
|
24
|
+
shellInvocation,
|
|
25
|
+
TASK_STATUS_VALUES,
|
|
26
|
+
taskDisplayName,
|
|
27
|
+
TERMINAL_TASK_STATUS_VALUES,
|
|
28
|
+
truncateChars,
|
|
29
|
+
} from '../types.js';
|
|
30
|
+
|
|
31
|
+
void describe('core', () => {
|
|
32
|
+
void it('parses /bg names, aliases, equals forms, quotes, and empty commands', () => {
|
|
33
|
+
assert.deepEqual(parseBgCommandArgs('--name "Build Docs" npm run docs'), {
|
|
34
|
+
name: 'Build Docs',
|
|
35
|
+
command: 'npm run docs',
|
|
36
|
+
isAgent: false,
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(parseBgCommandArgs('--name=Build npm test'), {
|
|
39
|
+
name: 'Build',
|
|
40
|
+
command: 'npm test',
|
|
41
|
+
isAgent: false,
|
|
42
|
+
});
|
|
43
|
+
assert.deepEqual(parseBgCommandArgs("-n 'Quoted Name' printf ok"), {
|
|
44
|
+
name: 'Quoted Name',
|
|
45
|
+
command: 'printf ok',
|
|
46
|
+
isAgent: false,
|
|
47
|
+
});
|
|
48
|
+
assert.deepEqual(parseBgCommandArgs('-n=One printf one'), {
|
|
49
|
+
name: 'One',
|
|
50
|
+
command: 'printf one',
|
|
51
|
+
isAgent: false,
|
|
52
|
+
});
|
|
53
|
+
assert.deepEqual(parseBgCommandArgs('--agent --name Agent pi -p hi'), {
|
|
54
|
+
name: 'Agent',
|
|
55
|
+
command: 'pi -p hi',
|
|
56
|
+
isAgent: true,
|
|
57
|
+
});
|
|
58
|
+
assert.deepEqual(parseBgCommandArgs('--name Agent --script pi -p hi'), {
|
|
59
|
+
name: 'Agent',
|
|
60
|
+
command: 'pi -p hi',
|
|
61
|
+
isAgent: false,
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(parseBgCommandArgs('echo ok'), { command: 'echo ok', isAgent: false });
|
|
64
|
+
assert.deepEqual(parseBgCommandArgs("--name 'Only Name'"), {
|
|
65
|
+
name: 'Only Name',
|
|
66
|
+
command: '',
|
|
67
|
+
isAgent: false,
|
|
68
|
+
});
|
|
69
|
+
assert.throws(() => parseBgCommandArgs('--name'), /requires a task name/);
|
|
70
|
+
assert.throws(() => parseBgCommandArgs('--name "unterminated'), /requires a task name/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
void it('normalizes task names and derives stable fallbacks', () => {
|
|
74
|
+
assert.equal(normalizeTaskName(' "A B" '), 'A B');
|
|
75
|
+
assert.equal(normalizeTaskName('\n\t'), undefined);
|
|
76
|
+
assert.equal(normalizeTaskName(123), undefined);
|
|
77
|
+
assert.equal(normalizeTaskName('x'.repeat(100)), `${'x'.repeat(79)}…`);
|
|
78
|
+
assert.equal(deriveTaskNameFromCommand('npm run test -- --watch'), 'npm run test');
|
|
79
|
+
assert.equal(deriveTaskNameFromCommand('pnpm build && echo done'), 'pnpm build');
|
|
80
|
+
assert.equal(deriveTaskNameFromCommand(''), 'Background task');
|
|
81
|
+
assert.equal(
|
|
82
|
+
taskDisplayName({ description: 'Longer description', command: 'echo ok' }),
|
|
83
|
+
'Longer description',
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
taskDisplayName({ command: 'echo one two three four five six' }),
|
|
87
|
+
'echo one two three four',
|
|
88
|
+
);
|
|
89
|
+
assert.equal(taskDisplayName({ id: 'b123' }), 'b123');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
void it('derives truthful completion delivery for every notification and wake combination', () => {
|
|
93
|
+
assert.deepEqual(deriveCompletionDeliveryGuidance(true, true), {
|
|
94
|
+
mode: 'notification-and-wake',
|
|
95
|
+
notificationEnabled: true,
|
|
96
|
+
automaticWakeEnabled: true,
|
|
97
|
+
text: [
|
|
98
|
+
'Terminal notification: enabled.',
|
|
99
|
+
'Automatic follow-up turn: enabled.',
|
|
100
|
+
'Next action: do not poll or sleep merely to wait; continue only independent useful work, otherwise end this turn and wait for <background-task-notification>.',
|
|
101
|
+
].join('\n'),
|
|
102
|
+
});
|
|
103
|
+
assert.deepEqual(deriveCompletionDeliveryGuidance(true, false), {
|
|
104
|
+
mode: 'notification-only',
|
|
105
|
+
notificationEnabled: true,
|
|
106
|
+
automaticWakeEnabled: false,
|
|
107
|
+
text: [
|
|
108
|
+
'Terminal notification: enabled.',
|
|
109
|
+
'Automatic follow-up turn: disabled. The terminal notification will be delivered, but it will not start an agent turn.',
|
|
110
|
+
'Next action: automatic wake-up was explicitly disabled; use bg_status/bg_logs only when deliberate monitoring is required, without tight polling.',
|
|
111
|
+
].join('\n'),
|
|
112
|
+
});
|
|
113
|
+
const notifyDisabledWithRequestedWake = deriveCompletionDeliveryGuidance(false, true);
|
|
114
|
+
assert.equal(notifyDisabledWithRequestedWake.mode, 'manual-monitoring');
|
|
115
|
+
assert.equal(notifyDisabledWithRequestedWake.notificationEnabled, false);
|
|
116
|
+
assert.equal(notifyDisabledWithRequestedWake.automaticWakeEnabled, false);
|
|
117
|
+
assert.match(notifyDisabledWithRequestedWake.text, /triggerOnCompletion has no effect/);
|
|
118
|
+
const manual = deriveCompletionDeliveryGuidance(false, false);
|
|
119
|
+
assert.equal(manual.mode, 'manual-monitoring');
|
|
120
|
+
assert.match(manual.text, /deliberate manual monitoring/);
|
|
121
|
+
assert.doesNotMatch(manual.text, /triggerOnCompletion has no effect/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
void it('formats durations, paths, snapshots, and byte limits', () => {
|
|
125
|
+
assert.equal(formatDuration(999), '999ms');
|
|
126
|
+
assert.equal(formatDuration(1000), '1s');
|
|
127
|
+
assert.equal(formatDuration(65_000), '1m5s');
|
|
128
|
+
assert.equal(formatDuration(3_660_000), '1h1m');
|
|
129
|
+
assert.equal(formatCompactNumber(999), '999');
|
|
130
|
+
assert.equal(formatCompactNumber(1250), '1.3k');
|
|
131
|
+
assert.equal(formatCompactNumber(42_000), '42k');
|
|
132
|
+
assert.equal(sanitizePathSegment('a/b c'), 'a-b-c');
|
|
133
|
+
assert.equal(sanitizePathSegment('///'), 'session');
|
|
134
|
+
// Pin both dialects explicitly. The host default differs by platform:
|
|
135
|
+
// cmd.exe requires the paired outer-quoted form, POSIX passes the command
|
|
136
|
+
// through verbatim, so an unpinned assertion fails on Windows.
|
|
137
|
+
assert.equal(shellInvocation('echo ok', 'linux', {}).args.includes('echo ok'), true);
|
|
138
|
+
assert.deepEqual(shellInvocation('echo ok', 'win32', { ComSpec: 'cmd.exe' }).args, [
|
|
139
|
+
'/d',
|
|
140
|
+
'/s',
|
|
141
|
+
'/c',
|
|
142
|
+
'"echo ok"',
|
|
143
|
+
]);
|
|
144
|
+
assert.equal(normalizeMaxBytes(-1, 123), 1);
|
|
145
|
+
assert.equal(normalizeMaxBytes(Number.NaN, 123), 123);
|
|
146
|
+
assert.equal(normalizeMaxBytes(1.9, 123), 1);
|
|
147
|
+
assert.equal(truncateChars('abcdef', 4), 'abc…');
|
|
148
|
+
assert.deepEqual([...TASK_STATUS_VALUES], ['running', 'completed', 'failed', 'killed']);
|
|
149
|
+
assert.deepEqual([...TERMINAL_TASK_STATUS_VALUES], ['completed', 'failed', 'killed']);
|
|
150
|
+
|
|
151
|
+
const text = formatSnapshotList(
|
|
152
|
+
[
|
|
153
|
+
{
|
|
154
|
+
id: 'b12345678',
|
|
155
|
+
name: 'Unit Task',
|
|
156
|
+
command: 'echo ok',
|
|
157
|
+
status: 'completed',
|
|
158
|
+
outputPath: '.pi/tasks/run/b12345678.output',
|
|
159
|
+
cwd: '/tmp',
|
|
160
|
+
startTime: 1000,
|
|
161
|
+
endTime: 2000,
|
|
162
|
+
exitCode: 0,
|
|
163
|
+
bytesWritten: 3,
|
|
164
|
+
isAgent: true,
|
|
165
|
+
notified: true,
|
|
166
|
+
notifyOnCompletion: true,
|
|
167
|
+
triggerOnCompletion: false,
|
|
168
|
+
contextUsage: { tokens: 1250, contextWindow: 200_000, percent: 0.625 },
|
|
169
|
+
tokenUsage: {
|
|
170
|
+
input: 1000,
|
|
171
|
+
output: 200,
|
|
172
|
+
cacheRead: 30,
|
|
173
|
+
cacheWrite: 20,
|
|
174
|
+
totalTokens: 1250,
|
|
175
|
+
},
|
|
176
|
+
toolUsage: { total: 2, failed: 1, byName: { read: 1, bash: 1 } },
|
|
177
|
+
model: 'anthropic/claude-sonnet-4',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'b99999999',
|
|
181
|
+
command: 'bad',
|
|
182
|
+
status: 'failed',
|
|
183
|
+
outputPath: '.pi/tasks/run/b99999999.output',
|
|
184
|
+
cwd: '/tmp',
|
|
185
|
+
startTime: 1000,
|
|
186
|
+
endTime: 2000,
|
|
187
|
+
exitCode: 1,
|
|
188
|
+
bytesWritten: 0,
|
|
189
|
+
isAgent: false,
|
|
190
|
+
error: 'x'.repeat(100),
|
|
191
|
+
notified: false,
|
|
192
|
+
notifyOnCompletion: true,
|
|
193
|
+
triggerOnCompletion: true,
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
2000,
|
|
197
|
+
);
|
|
198
|
+
assert.match(text, /Unit Task/);
|
|
199
|
+
assert.match(text, /ctx=0\.6%\/200k/);
|
|
200
|
+
assert.match(text, /model=anthropic\/claude-sonnet-4/);
|
|
201
|
+
assert.match(text, /tokens=1\.3k/);
|
|
202
|
+
assert.match(text, /tools=2 failed=1/);
|
|
203
|
+
assert.match(text, /✗ b99999999 failed/);
|
|
204
|
+
assert.match(text, /output: \.pi\/tasks/);
|
|
205
|
+
assert.equal(
|
|
206
|
+
formatModelSummary('anthropic/claude-sonnet-4'),
|
|
207
|
+
'model=anthropic/claude-sonnet-4',
|
|
208
|
+
);
|
|
209
|
+
assert.equal(formatModelSummary(undefined), undefined);
|
|
210
|
+
assert.equal(formatModelSummary(''), undefined);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
void it('parses and compares semver including prerelease precedence', () => {
|
|
214
|
+
assert.deepEqual(parseSemver('1.2.3'), { major: 1, minor: 2, patch: 3, prerelease: [] });
|
|
215
|
+
assert.deepEqual(parseSemver('v0.4.0'), { major: 0, minor: 4, patch: 0, prerelease: [] });
|
|
216
|
+
assert.deepEqual(parseSemver('1.0.0-beta.2'), {
|
|
217
|
+
major: 1,
|
|
218
|
+
minor: 0,
|
|
219
|
+
patch: 0,
|
|
220
|
+
prerelease: ['beta', '2'],
|
|
221
|
+
});
|
|
222
|
+
assert.deepEqual(parseSemver('1.2.3+build.5'), {
|
|
223
|
+
major: 1,
|
|
224
|
+
minor: 2,
|
|
225
|
+
patch: 3,
|
|
226
|
+
prerelease: [],
|
|
227
|
+
});
|
|
228
|
+
assert.equal(parseSemver('1.2'), undefined);
|
|
229
|
+
assert.equal(parseSemver('latest'), undefined);
|
|
230
|
+
assert.equal(parseSemver(''), undefined);
|
|
231
|
+
|
|
232
|
+
assert.equal(compareSemver('1.2.3', '1.2.2'), 1);
|
|
233
|
+
assert.equal(compareSemver('1.2.3', '1.2.3'), 0);
|
|
234
|
+
assert.equal(compareSemver('1.2.3', '1.3.0'), -1);
|
|
235
|
+
assert.equal(compareSemver('2.0.0', '1.9.9'), 1);
|
|
236
|
+
assert.equal(compareSemver('1.0.0', '1.0.0-beta'), 1);
|
|
237
|
+
assert.equal(compareSemver('1.0.0-alpha', '1.0.0'), -1);
|
|
238
|
+
assert.equal(compareSemver('1.0.0-alpha', '1.0.0-beta'), -1);
|
|
239
|
+
assert.equal(compareSemver('1.0.0-alpha.2', '1.0.0-alpha.1'), 1);
|
|
240
|
+
assert.equal(compareSemver('1.0.0-alpha.1', '1.0.0-alpha'), 1);
|
|
241
|
+
assert.equal(compareSemver('1.0.0-1', '1.0.0-alpha'), -1);
|
|
242
|
+
assert.equal(compareSemver('1.0.0-2', '1.0.0-10'), -1);
|
|
243
|
+
assert.equal(compareSemver('garbage', '1.0.0'), undefined);
|
|
244
|
+
assert.equal(compareSemver('1.0.0', 'garbage'), undefined);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
void it('derives newer-version flags and footer update segments', () => {
|
|
248
|
+
assert.equal(isNewerVersion('0.4.0', '0.3.0'), true);
|
|
249
|
+
assert.equal(isNewerVersion('v0.4.0', '0.3.0'), true);
|
|
250
|
+
assert.equal(isNewerVersion('0.3.0', '0.3.0'), false);
|
|
251
|
+
assert.equal(isNewerVersion('0.2.0', '0.3.0'), false);
|
|
252
|
+
assert.equal(isNewerVersion('garbage', '0.3.0'), false);
|
|
253
|
+
assert.equal(formatUpdateSegment('0.4.0', '0.3.0'), '\u2b06 v0.4.0 /unipi:bg-update');
|
|
254
|
+
assert.equal(formatUpdateSegment('0.3.0', '0.3.0'), undefined);
|
|
255
|
+
assert.equal(formatUpdateSegment('0.2.0', '0.3.0'), undefined);
|
|
256
|
+
assert.equal(formatUpdateSegment(undefined, '0.3.0'), undefined);
|
|
257
|
+
assert.equal(formatUpdateSegment('garbage', '0.3.0'), undefined);
|
|
258
|
+
assert.equal(formatUpdateSegment('0.4.0', ''), undefined);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
void it('applies the Windows shell dialect policy', async () => {
|
|
262
|
+
const root = await mkdtemp(join(tmpdir(), 'pi-bg-shell-'));
|
|
263
|
+
try {
|
|
264
|
+
const bashPath = join(root, 'bash.exe');
|
|
265
|
+
const cmdPath = join(root, 'cmd.com');
|
|
266
|
+
await writeFile(bashPath, '', 'utf8');
|
|
267
|
+
await writeFile(cmdPath, '', 'utf8');
|
|
268
|
+
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
shellInvocation('echo %USERPROFILE%', 'win32', {
|
|
271
|
+
SHELL: bashPath,
|
|
272
|
+
ComSpec: 'C:\\Windows\\System32\\cmd.exe',
|
|
273
|
+
}),
|
|
274
|
+
{
|
|
275
|
+
shell: 'C:\\Windows\\System32\\cmd.exe',
|
|
276
|
+
args: ['/d', '/s', '/c', '"echo %USERPROFILE%"'],
|
|
277
|
+
dialect: 'cmd',
|
|
278
|
+
windowsVerbatimArguments: true,
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
assert.deepEqual(shellInvocation('dir C:\\work', 'win32', {}), {
|
|
282
|
+
shell: 'cmd.exe',
|
|
283
|
+
args: ['/d', '/s', '/c', '"dir C:\\work"'],
|
|
284
|
+
dialect: 'cmd',
|
|
285
|
+
windowsVerbatimArguments: true,
|
|
286
|
+
});
|
|
287
|
+
assert.deepEqual(
|
|
288
|
+
shellInvocation('printf ok', 'win32', {
|
|
289
|
+
UNIPI_BG_SHELL: 'bash',
|
|
290
|
+
UNIPI_BG_SHELL_PATH: bashPath,
|
|
291
|
+
}),
|
|
292
|
+
{
|
|
293
|
+
shell: bashPath,
|
|
294
|
+
args: ['-c', 'printf ok'],
|
|
295
|
+
dialect: 'posix',
|
|
296
|
+
windowsVerbatimArguments: false,
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
assert.notDeepEqual(
|
|
300
|
+
shellInvocation('printf ok', 'win32', {
|
|
301
|
+
UNIPI_BG_SHELL: 'bash',
|
|
302
|
+
UNIPI_BG_SHELL_PATH: bashPath,
|
|
303
|
+
}).args,
|
|
304
|
+
['-lc', 'printf ok'],
|
|
305
|
+
);
|
|
306
|
+
assert.equal(
|
|
307
|
+
shellInvocation('printf ok', 'win32', { UNIPI_BG_SHELL: 'bash', PATH: root }).shell,
|
|
308
|
+
bashPath,
|
|
309
|
+
);
|
|
310
|
+
assert.equal(
|
|
311
|
+
shellInvocation('set X=1', 'win32', { UNIPI_BG_SHELL: 'cmd', UNIPI_BG_SHELL_PATH: cmdPath })
|
|
312
|
+
.shell,
|
|
313
|
+
cmdPath,
|
|
314
|
+
);
|
|
315
|
+
for (const value of ['', ' bash', 'bash ', 'zsh']) {
|
|
316
|
+
assert.throws(() => shellInvocation('echo bad', 'win32', { UNIPI_BG_SHELL: value }), /cmd or bash/);
|
|
317
|
+
}
|
|
318
|
+
assert.throws(
|
|
319
|
+
() => shellInvocation('echo bad', 'win32', { UNIPI_BG_SHELL_PATH: bashPath }),
|
|
320
|
+
/requires UNIPI_BG_SHELL/,
|
|
321
|
+
);
|
|
322
|
+
assert.throws(
|
|
323
|
+
() =>
|
|
324
|
+
shellInvocation('echo bad', 'win32', {
|
|
325
|
+
UNIPI_BG_SHELL: 'bash',
|
|
326
|
+
UNIPI_BG_SHELL_PATH: 'bash.exe',
|
|
327
|
+
}),
|
|
328
|
+
/absolute path/,
|
|
329
|
+
);
|
|
330
|
+
assert.deepEqual(
|
|
331
|
+
shellInvocation('echo $SHELL', 'linux', {
|
|
332
|
+
SHELL: '/bin/zsh',
|
|
333
|
+
UNIPI_BG_SHELL: 'cmd',
|
|
334
|
+
UNIPI_BG_SHELL_PATH: bashPath,
|
|
335
|
+
}),
|
|
336
|
+
{
|
|
337
|
+
shell: '/bin/zsh',
|
|
338
|
+
args: ['-c', 'echo $SHELL'],
|
|
339
|
+
dialect: 'posix',
|
|
340
|
+
windowsVerbatimArguments: false,
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
} finally {
|
|
344
|
+
await rm(root, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
void it('parses and formats agent activity transcript lines', () => {
|
|
349
|
+
assert.deepEqual(
|
|
350
|
+
parseAgentActivity({
|
|
351
|
+
type: 'background-task-activity',
|
|
352
|
+
kind: 'assistant_text',
|
|
353
|
+
text: 'Done.\n',
|
|
354
|
+
}),
|
|
355
|
+
{ kind: 'assistant_text', text: 'Done.\n' },
|
|
356
|
+
);
|
|
357
|
+
assert.equal(
|
|
358
|
+
formatAgentActivityLine({ kind: 'assistant_text', text: 'Looks good\n\n' }),
|
|
359
|
+
'Looks good',
|
|
360
|
+
);
|
|
361
|
+
assert.equal(formatAgentActivityLine({ kind: 'assistant_text', text: ' \n ' }), undefined);
|
|
362
|
+
|
|
363
|
+
assert.equal(
|
|
364
|
+
formatAgentActivityLine({ kind: 'reasoning', text: 'weighing options' }),
|
|
365
|
+
'\u2026 weighing options',
|
|
366
|
+
);
|
|
367
|
+
assert.equal(formatAgentActivityLine({ kind: 'reasoning', text: ' ' }), undefined);
|
|
368
|
+
|
|
369
|
+
assert.deepEqual(
|
|
370
|
+
parseAgentActivity({
|
|
371
|
+
type: 'background-task-activity',
|
|
372
|
+
kind: 'tool_start',
|
|
373
|
+
tool: 'read',
|
|
374
|
+
argsSummary: 'README.md',
|
|
375
|
+
}),
|
|
376
|
+
{ kind: 'tool_start', tool: 'read', argsSummary: 'README.md' },
|
|
377
|
+
);
|
|
378
|
+
assert.equal(
|
|
379
|
+
formatAgentActivityLine({
|
|
380
|
+
kind: 'tool_start',
|
|
381
|
+
tool: 'read',
|
|
382
|
+
argsSummary: ' src/index.ts ',
|
|
383
|
+
}),
|
|
384
|
+
'\u2192 read src/index.ts',
|
|
385
|
+
);
|
|
386
|
+
assert.equal(
|
|
387
|
+
formatAgentActivityLine({ kind: 'tool_start', tool: 'bash', argsSummary: '' }),
|
|
388
|
+
'\u2192 bash',
|
|
389
|
+
);
|
|
390
|
+
assert.equal(
|
|
391
|
+
formatAgentActivityLine({ kind: 'tool_start', tool: 'bash', argsSummary: 'x'.repeat(120) }),
|
|
392
|
+
`\u2192 bash ${'x'.repeat(79)}\u2026`,
|
|
393
|
+
);
|
|
394
|
+
assert.deepEqual(
|
|
395
|
+
parseAgentActivity({ type: 'background-task-activity', kind: 'tool_start', tool: 'ls' }),
|
|
396
|
+
{ kind: 'tool_start', tool: 'ls', argsSummary: '' },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
assert.deepEqual(
|
|
400
|
+
parseAgentActivity({
|
|
401
|
+
type: 'background-task-activity',
|
|
402
|
+
kind: 'tool_end',
|
|
403
|
+
tool: 'bash',
|
|
404
|
+
isError: true,
|
|
405
|
+
error: 'boom',
|
|
406
|
+
}),
|
|
407
|
+
{ kind: 'tool_end', tool: 'bash', isError: true, error: 'boom' },
|
|
408
|
+
);
|
|
409
|
+
assert.equal(
|
|
410
|
+
formatAgentActivityLine({ kind: 'tool_end', tool: 'read', isError: false }),
|
|
411
|
+
undefined,
|
|
412
|
+
);
|
|
413
|
+
assert.equal(
|
|
414
|
+
formatAgentActivityLine({ kind: 'tool_end', tool: 'bash', isError: true }),
|
|
415
|
+
'\u2717 bash failed',
|
|
416
|
+
);
|
|
417
|
+
assert.equal(
|
|
418
|
+
formatAgentActivityLine({
|
|
419
|
+
kind: 'tool_end',
|
|
420
|
+
tool: 'bash',
|
|
421
|
+
isError: true,
|
|
422
|
+
error: 'exit 1\nmore',
|
|
423
|
+
}),
|
|
424
|
+
'\u2717 bash failed: exit 1 more',
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
assert.equal(parseAgentActivity(null), undefined);
|
|
428
|
+
assert.equal(parseAgentActivity('background-task-activity'), undefined);
|
|
429
|
+
assert.equal(
|
|
430
|
+
parseAgentActivity({ type: 'background-task-telemetry', kind: 'tool_start', tool: 'read' }),
|
|
431
|
+
undefined,
|
|
432
|
+
);
|
|
433
|
+
assert.equal(
|
|
434
|
+
parseAgentActivity({ type: 'background-task-activity', kind: 'mystery' }),
|
|
435
|
+
undefined,
|
|
436
|
+
);
|
|
437
|
+
assert.equal(
|
|
438
|
+
parseAgentActivity({ type: 'background-task-activity', kind: 'tool_start' }),
|
|
439
|
+
undefined,
|
|
440
|
+
);
|
|
441
|
+
assert.equal(
|
|
442
|
+
parseAgentActivity({ type: 'background-task-activity', kind: 'assistant_text' }),
|
|
443
|
+
undefined,
|
|
444
|
+
);
|
|
445
|
+
const end = parseAgentActivity({
|
|
446
|
+
type: 'background-task-activity',
|
|
447
|
+
kind: 'tool_end',
|
|
448
|
+
tool: 'x',
|
|
449
|
+
isError: false,
|
|
450
|
+
error: ' ',
|
|
451
|
+
});
|
|
452
|
+
assert.equal(end?.kind, 'tool_end');
|
|
453
|
+
assert.equal(end?.tool, 'x');
|
|
454
|
+
assert.equal(end?.isError, false);
|
|
455
|
+
assert.ok(!end || !('error' in end) || end.error === undefined);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
void it('boundedRead supports head, tail, truncation, and empty files', async () => {
|
|
459
|
+
const dir = await mkdtemp(join(tmpdir(), 'pi-bg-unit-'));
|
|
460
|
+
try {
|
|
461
|
+
const f = join(dir, 'out');
|
|
462
|
+
await writeFile(f, 'abcdef');
|
|
463
|
+
assert.deepEqual(await boundedRead(f, 3, true), {
|
|
464
|
+
content: 'def',
|
|
465
|
+
truncated: true,
|
|
466
|
+
bytesRead: 3,
|
|
467
|
+
totalBytes: 6,
|
|
468
|
+
});
|
|
469
|
+
assert.deepEqual(await boundedRead(f, 3, false), {
|
|
470
|
+
content: 'abc',
|
|
471
|
+
truncated: true,
|
|
472
|
+
bytesRead: 3,
|
|
473
|
+
totalBytes: 6,
|
|
474
|
+
});
|
|
475
|
+
assert.deepEqual(await boundedRead(f, 99, false), {
|
|
476
|
+
content: 'abcdef',
|
|
477
|
+
truncated: false,
|
|
478
|
+
bytesRead: 6,
|
|
479
|
+
totalBytes: 6,
|
|
480
|
+
});
|
|
481
|
+
const empty = join(dir, 'empty');
|
|
482
|
+
await writeFile(empty, '');
|
|
483
|
+
assert.deepEqual(await boundedRead(empty, 3, true), {
|
|
484
|
+
content: '',
|
|
485
|
+
truncated: false,
|
|
486
|
+
bytesRead: 0,
|
|
487
|
+
totalBytes: 0,
|
|
488
|
+
});
|
|
489
|
+
} finally {
|
|
490
|
+
await rm(dir, { recursive: true, force: true });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
});
|