pi-mono-all 1.0.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/CHANGELOG.md +13 -0
- package/LICENCE.md +7 -0
- package/node_modules/pi-common/package.json +22 -0
- package/node_modules/pi-common/src/auth-config.ts +290 -0
- package/node_modules/pi-common/src/auth.ts +63 -0
- package/node_modules/pi-common/src/cache.ts +60 -0
- package/node_modules/pi-common/src/errors.ts +47 -0
- package/node_modules/pi-common/src/http-client.ts +118 -0
- package/node_modules/pi-common/src/index.ts +7 -0
- package/node_modules/pi-common/src/rate-limiter.ts +32 -0
- package/node_modules/pi-common/src/tool-result.ts +27 -0
- package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
- package/node_modules/pi-mono-ask-user-question/README.md +226 -0
- package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
- package/node_modules/pi-mono-ask-user-question/package.json +29 -0
- package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-auto-fix/README.md +77 -0
- package/node_modules/pi-mono-auto-fix/index.ts +488 -0
- package/node_modules/pi-mono-auto-fix/package.json +23 -0
- package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-btw/README.md +24 -0
- package/node_modules/pi-mono-btw/index.ts +499 -0
- package/node_modules/pi-mono-btw/package.json +29 -0
- package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-clear/README.md +40 -0
- package/node_modules/pi-mono-clear/index.ts +45 -0
- package/node_modules/pi-mono-clear/package.json +29 -0
- package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
- package/node_modules/pi-mono-context/README.md +74 -0
- package/node_modules/pi-mono-context/index.ts +641 -0
- package/node_modules/pi-mono-context/package.json +29 -0
- package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
- package/node_modules/pi-mono-context-guard/README.md +81 -0
- package/node_modules/pi-mono-context-guard/index.ts +212 -0
- package/node_modules/pi-mono-context-guard/package.json +23 -0
- package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
- package/node_modules/pi-mono-figma/README.md +236 -0
- package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
- package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
- package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
- package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
- package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
- package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
- package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
- package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
- package/node_modules/pi-mono-figma/index.ts +6 -0
- package/node_modules/pi-mono-figma/package.json +33 -0
- package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
- package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
- package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
- package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
- package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
- package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
- package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
- package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
- package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
- package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
- package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
- package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
- package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
- package/node_modules/pi-mono-linear/README.md +159 -0
- package/node_modules/pi-mono-linear/index.ts +6 -0
- package/node_modules/pi-mono-linear/package.json +30 -0
- package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
- package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
- package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
- package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
- package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
- package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-loop/README.md +54 -0
- package/node_modules/pi-mono-loop/index.ts +291 -0
- package/node_modules/pi-mono-loop/package.json +26 -0
- package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
- package/node_modules/pi-mono-multi-edit/README.md +244 -0
- package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
- package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
- package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
- package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
- package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
- package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
- package/node_modules/pi-mono-multi-edit/index.ts +266 -0
- package/node_modules/pi-mono-multi-edit/package.json +37 -0
- package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
- package/node_modules/pi-mono-multi-edit/types.ts +53 -0
- package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
- package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
- package/node_modules/pi-mono-review/README.md +30 -0
- package/node_modules/pi-mono-review/common.ts +930 -0
- package/node_modules/pi-mono-review/index.ts +8 -0
- package/node_modules/pi-mono-review/package.json +29 -0
- package/node_modules/pi-mono-review/review-tui.ts +194 -0
- package/node_modules/pi-mono-review/review.ts +119 -0
- package/node_modules/pi-mono-review/reviewer.ts +339 -0
- package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
- package/node_modules/pi-mono-sentinel/README.md +87 -0
- package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
- package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
- package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
- package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
- package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
- package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
- package/node_modules/pi-mono-sentinel/index.ts +43 -0
- package/node_modules/pi-mono-sentinel/package.json +26 -0
- package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
- package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
- package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
- package/node_modules/pi-mono-sentinel/session.ts +95 -0
- package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
- package/node_modules/pi-mono-sentinel/types.ts +39 -0
- package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
- package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
- package/node_modules/pi-mono-simplify/README.md +56 -0
- package/node_modules/pi-mono-simplify/index.ts +78 -0
- package/node_modules/pi-mono-simplify/package.json +29 -0
- package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
- package/node_modules/pi-mono-status-line/README.md +96 -0
- package/node_modules/pi-mono-status-line/basic.ts +89 -0
- package/node_modules/pi-mono-status-line/expert.ts +689 -0
- package/node_modules/pi-mono-status-line/index.ts +54 -0
- package/node_modules/pi-mono-status-line/package.json +29 -0
- package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
- package/node_modules/pi-mono-team-mode/README.md +246 -0
- package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
- package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
- package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
- package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
- package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
- package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
- package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
- package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
- package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
- package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
- package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
- package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
- package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
- package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
- package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
- package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
- package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
- package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
- package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
- package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
- package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
- package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
- package/node_modules/pi-mono-team-mode/index.ts +825 -0
- package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
- package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
- package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
- package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
- package/node_modules/pi-mono-team-mode/package.json +33 -0
- package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
- package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
- package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
- package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
- package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
- package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
- package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, test } from "node:test";
|
|
6
|
+
|
|
7
|
+
import { TeamMateStore } from "../core/store.ts";
|
|
8
|
+
import { AgentManager } from "../managers/agent-manager.ts";
|
|
9
|
+
|
|
10
|
+
async function withManager(fn: (manager: AgentManager, store: TeamMateStore) => Promise<void>) {
|
|
11
|
+
const root = await mkdtemp(path.join(tmpdir(), "team-mode-test-"));
|
|
12
|
+
try {
|
|
13
|
+
const store = new TeamMateStore(root);
|
|
14
|
+
const manager = new AgentManager({
|
|
15
|
+
store,
|
|
16
|
+
getParentSessionId: () => "parent",
|
|
17
|
+
getDefaultCwd: () => process.cwd(),
|
|
18
|
+
runTransientSession: async (opts) => ({
|
|
19
|
+
teammateId: opts.id,
|
|
20
|
+
name: opts.name,
|
|
21
|
+
description: opts.description,
|
|
22
|
+
status: "completed",
|
|
23
|
+
result: `TRANSIENT:${opts.message}`,
|
|
24
|
+
exitCode: 0,
|
|
25
|
+
provider: opts.provider,
|
|
26
|
+
model: opts.model,
|
|
27
|
+
thinkingLevel: opts.thinkingLevel,
|
|
28
|
+
modelRationale: opts.modelRationale,
|
|
29
|
+
runtime: "transient",
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
await fn(manager, store);
|
|
33
|
+
} finally {
|
|
34
|
+
await rm(root, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("AgentManager transient runtime", () => {
|
|
39
|
+
test("routes transient spawn without durable teammate record", async () => {
|
|
40
|
+
await withManager(async (manager, store) => {
|
|
41
|
+
const result = await manager.spawn({
|
|
42
|
+
description: "quick scan",
|
|
43
|
+
prompt: "Summarize files",
|
|
44
|
+
runtime: "transient",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(result.status, "completed");
|
|
48
|
+
assert.equal(result.runtime, "transient");
|
|
49
|
+
assert.match(result.result, /Task: quick scan/);
|
|
50
|
+
assert.deepEqual(await store.listTeammates(), []);
|
|
51
|
+
assert.deepEqual(await store.getNameIndex("parent"), {});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("rejects transient-incompatible options", async () => {
|
|
56
|
+
await withManager(async (manager) => {
|
|
57
|
+
await assert.rejects(
|
|
58
|
+
() => manager.spawn({ description: "x", prompt: "p", runtime: "transient", isolation: "worktree" }),
|
|
59
|
+
/does not support isolation "worktree"/,
|
|
60
|
+
);
|
|
61
|
+
await assert.rejects(
|
|
62
|
+
() => manager.spawn({ description: "x", prompt: "p", runtime: "transient", background: true }),
|
|
63
|
+
/does not support run_in_background/,
|
|
64
|
+
);
|
|
65
|
+
await assert.rejects(
|
|
66
|
+
() => manager.spawn({ description: "x", prompt: "p", runtime: "transient", teamId: "team-1" }),
|
|
67
|
+
/does not support team_name/,
|
|
68
|
+
);
|
|
69
|
+
await assert.rejects(
|
|
70
|
+
() => manager.spawn({ description: "x", prompt: "p", runtime: "transient", name: "later" }),
|
|
71
|
+
/does not support name/,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { DelegationManager } from "../managers/delegation-manager.ts";
|
|
5
|
+
|
|
6
|
+
type SpawnCall = {
|
|
7
|
+
description: string;
|
|
8
|
+
prompt: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
thinkingLevel?: string;
|
|
11
|
+
runtime?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function makeFakeAgents() {
|
|
15
|
+
const calls: SpawnCall[] = [];
|
|
16
|
+
let queued = 0;
|
|
17
|
+
let idx = 0;
|
|
18
|
+
return {
|
|
19
|
+
calls,
|
|
20
|
+
get queued() {
|
|
21
|
+
return queued;
|
|
22
|
+
},
|
|
23
|
+
setQueuedCount(value: number) {
|
|
24
|
+
queued = value;
|
|
25
|
+
},
|
|
26
|
+
async spawn(opts: SpawnCall & Record<string, unknown>) {
|
|
27
|
+
calls.push(opts);
|
|
28
|
+
idx += 1;
|
|
29
|
+
return {
|
|
30
|
+
teammateId: `agent-${idx}`,
|
|
31
|
+
name: opts.name ?? `agent-${idx}`,
|
|
32
|
+
description: opts.description,
|
|
33
|
+
status: "completed" as const,
|
|
34
|
+
result: `RESULT:${opts.prompt}`,
|
|
35
|
+
exitCode: 0,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("DelegationManager", () => {
|
|
42
|
+
test("runParallel expands count and aggregates", async () => {
|
|
43
|
+
const fake = makeFakeAgents();
|
|
44
|
+
const manager = new DelegationManager(fake as never);
|
|
45
|
+
const result = await manager.runParallel({
|
|
46
|
+
tasks: [{ description: "scan", prompt: "P", name: "worker", count: 2, thinkingLevel: "low" }],
|
|
47
|
+
concurrency: 2,
|
|
48
|
+
});
|
|
49
|
+
assert.equal(result.mode, "parallel");
|
|
50
|
+
assert.equal(result.steps, 2);
|
|
51
|
+
assert.equal(fake.calls.length, 2);
|
|
52
|
+
assert.equal(fake.calls[0]?.name, "worker-1");
|
|
53
|
+
assert.equal(fake.calls[1]?.name, "worker-2");
|
|
54
|
+
assert.equal(fake.calls[0]?.thinkingLevel, "low");
|
|
55
|
+
assert.match(result.output, /Parallel Task 1/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("runChain applies templates and chain_dir files", async () => {
|
|
59
|
+
const fake = makeFakeAgents();
|
|
60
|
+
const manager = new DelegationManager(fake as never);
|
|
61
|
+
const result = await manager.runChain({
|
|
62
|
+
task: "TOP",
|
|
63
|
+
chain: [
|
|
64
|
+
{
|
|
65
|
+
description: "one",
|
|
66
|
+
prompt: "Task={task}",
|
|
67
|
+
output: "step1.txt",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: "two",
|
|
71
|
+
reads: ["step1.txt"],
|
|
72
|
+
prompt: "Prev={previous}",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
assert.equal(result.mode, "chain");
|
|
77
|
+
assert.ok(result.chainDir);
|
|
78
|
+
assert.equal(fake.calls.length, 2);
|
|
79
|
+
assert.match(fake.calls[0]?.prompt ?? "", /Task=TOP/);
|
|
80
|
+
assert.match(fake.calls[1]?.prompt ?? "", /--- step1\.txt ---/);
|
|
81
|
+
assert.match(fake.calls[1]?.prompt ?? "", /RESULT:Task=TOP/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("runParallel applies top-level transient runtime and task overrides", async () => {
|
|
85
|
+
const fake = makeFakeAgents();
|
|
86
|
+
const manager = new DelegationManager(fake as never);
|
|
87
|
+
await manager.runParallel({
|
|
88
|
+
runtime: "transient",
|
|
89
|
+
tasks: [
|
|
90
|
+
{ description: "fast", prompt: "P1" },
|
|
91
|
+
{ description: "durable", prompt: "P2", runtime: "subprocess" },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
assert.equal(fake.calls[0]?.runtime, "transient");
|
|
95
|
+
assert.equal(fake.calls[1]?.runtime, "subprocess");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("runChain forwards transient runtime to chain and parallel steps", async () => {
|
|
99
|
+
const fake = makeFakeAgents();
|
|
100
|
+
const manager = new DelegationManager(fake as never);
|
|
101
|
+
await manager.runChain({
|
|
102
|
+
task: "TOP",
|
|
103
|
+
runtime: "transient",
|
|
104
|
+
chain: [
|
|
105
|
+
{ description: "one", prompt: "P1" },
|
|
106
|
+
{
|
|
107
|
+
parallel: [
|
|
108
|
+
{ description: "two", prompt: "P2" },
|
|
109
|
+
{ description: "three", prompt: "P3", runtime: "subprocess" },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
assert.equal(fake.calls[0]?.runtime, "transient");
|
|
115
|
+
assert.equal(fake.calls[1]?.runtime, "transient");
|
|
116
|
+
assert.equal(fake.calls[2]?.runtime, "subprocess");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Team-Mode — Formatter Tests (pure functions, no I/O)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { describe, test } from "node:test";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
formatTeamDashboard,
|
|
10
|
+
formatTeammateLine,
|
|
11
|
+
formatTeammateList,
|
|
12
|
+
formatTeammateStatus,
|
|
13
|
+
} from "../ui/formatters.ts";
|
|
14
|
+
import type { TeamRecord, TeammateRecord } from "../core/types.ts";
|
|
15
|
+
|
|
16
|
+
function tm(overrides: Partial<TeammateRecord> = {}): TeammateRecord {
|
|
17
|
+
return {
|
|
18
|
+
id: "researcher-abc",
|
|
19
|
+
name: "researcher",
|
|
20
|
+
isolation: "none",
|
|
21
|
+
cwd: "/tmp",
|
|
22
|
+
status: "running",
|
|
23
|
+
background: false,
|
|
24
|
+
createdAt: "2026-04-23T00:00:00Z",
|
|
25
|
+
updatedAt: "2026-04-23T00:00:00Z",
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function tm2(): TeamRecord {
|
|
31
|
+
return {
|
|
32
|
+
id: "billing-abc",
|
|
33
|
+
name: "billing",
|
|
34
|
+
createdAt: "2026-04-23T00:00:00Z",
|
|
35
|
+
defaultIsolation: "worktree",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("formatTeammateLine", () => {
|
|
40
|
+
test("includes name, status icon, role, team, isolation", () => {
|
|
41
|
+
const line = formatTeammateLine(
|
|
42
|
+
tm({ subagentType: "researcher", teamId: "billing-abc", isolation: "worktree" }),
|
|
43
|
+
);
|
|
44
|
+
assert.match(line, /researcher/);
|
|
45
|
+
assert.match(line, /\[researcher\]/);
|
|
46
|
+
assert.match(line, /team=billing-abc/);
|
|
47
|
+
assert.match(line, /wt/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("running shows ▸ icon", () => {
|
|
51
|
+
assert.match(formatTeammateLine(tm({ status: "running" })), /▸/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("completed shows ✓ icon", () => {
|
|
55
|
+
assert.match(formatTeammateLine(tm({ status: "completed" })), /✓/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("failed shows ✗ icon", () => {
|
|
59
|
+
assert.match(formatTeammateLine(tm({ status: "failed" })), /✗/);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("formatTeammateList", () => {
|
|
64
|
+
test("empty list shows placeholder", () => {
|
|
65
|
+
assert.equal(formatTeammateList([]), "No teammates.");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("renders each teammate on its own line", () => {
|
|
69
|
+
const out = formatTeammateList([tm({ name: "a" }), tm({ name: "b" })]);
|
|
70
|
+
assert.equal(out.split("\n").length, 2);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("formatTeamDashboard", () => {
|
|
75
|
+
test("groups teammates under their team", () => {
|
|
76
|
+
const out = formatTeamDashboard(
|
|
77
|
+
[tm2()],
|
|
78
|
+
[tm({ teamId: "billing-abc", name: "writer" })],
|
|
79
|
+
);
|
|
80
|
+
assert.match(out, /Teams:/);
|
|
81
|
+
assert.match(out, /billing/);
|
|
82
|
+
assert.match(out, /writer/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("shows unassigned teammates separately", () => {
|
|
86
|
+
const out = formatTeamDashboard([], [tm({ name: "orphan" })]);
|
|
87
|
+
assert.match(out, /Unassigned teammates:/);
|
|
88
|
+
assert.match(out, /orphan/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("empty state", () => {
|
|
92
|
+
assert.match(formatTeamDashboard([], []), /No teams and no teammates\./);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("formatTeammateStatus", () => {
|
|
97
|
+
test("prints multi-line status block", () => {
|
|
98
|
+
const out = formatTeammateStatus(tm({ lastExitCode: 0, lastResult: "done" }));
|
|
99
|
+
assert.match(out, /Teammate: researcher/);
|
|
100
|
+
assert.match(out, /Status: running/);
|
|
101
|
+
assert.match(out, /Last exit: 0/);
|
|
102
|
+
assert.match(out, /Last result:\ndone/);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Team-Mode — Model Config Tests
|
|
3
|
+
*
|
|
4
|
+
* Covers loadModelConfig / resolveModel / detectProvider and asserts the
|
|
5
|
+
* exact shape the user keeps at ~/.pi/agent/extensions/team-mode/model-config.json
|
|
6
|
+
* resolves to openai-codex/gpt-5.4-{mini,regular,:high} by role/tier.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { describe, test } from "node:test";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_MODEL_CONFIG,
|
|
17
|
+
detectProvider,
|
|
18
|
+
isModelTier,
|
|
19
|
+
loadModelConfig,
|
|
20
|
+
modelConfigPath,
|
|
21
|
+
resolveModel,
|
|
22
|
+
saveModelConfig,
|
|
23
|
+
type ModelConfig,
|
|
24
|
+
} from "../core/model-config.ts";
|
|
25
|
+
|
|
26
|
+
function withEnv<T>(patch: NodeJS.ProcessEnv, fn: () => T): T {
|
|
27
|
+
const prev: NodeJS.ProcessEnv = {};
|
|
28
|
+
for (const k of Object.keys(patch)) {
|
|
29
|
+
prev[k] = process.env[k];
|
|
30
|
+
if (patch[k] === undefined) delete process.env[k];
|
|
31
|
+
else process.env[k] = patch[k];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return fn();
|
|
35
|
+
} finally {
|
|
36
|
+
for (const k of Object.keys(patch)) {
|
|
37
|
+
if (prev[k] === undefined) delete process.env[k];
|
|
38
|
+
else process.env[k] = prev[k];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const USER_CONFIG: ModelConfig = {
|
|
44
|
+
provider: "openai-codex",
|
|
45
|
+
providers: {
|
|
46
|
+
anthropic: {
|
|
47
|
+
cheap: "anthropic/claude-haiku-4-5",
|
|
48
|
+
mid: "anthropic/claude-sonnet-4-6",
|
|
49
|
+
deep: "anthropic/claude-opus-4-7:high",
|
|
50
|
+
},
|
|
51
|
+
"openai-codex": {
|
|
52
|
+
cheap: "openai-codex/gpt-5.4-mini",
|
|
53
|
+
mid: "openai-codex/gpt-5.4",
|
|
54
|
+
deep: "openai-codex/gpt-5.4:high",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
tiers: {
|
|
58
|
+
...DEFAULT_MODEL_CONFIG.tiers,
|
|
59
|
+
cheap: { name: "Cheap", thinkingLevel: "minimal" },
|
|
60
|
+
mid: { name: "Mid", thinkingLevel: "medium" },
|
|
61
|
+
deep: { name: "Deep", thinkingLevel: "high" },
|
|
62
|
+
},
|
|
63
|
+
roles: {
|
|
64
|
+
...DEFAULT_MODEL_CONFIG.roles,
|
|
65
|
+
researcher: "cheap",
|
|
66
|
+
docs: "cheap",
|
|
67
|
+
backend: "mid",
|
|
68
|
+
frontend: "mid",
|
|
69
|
+
tester: "mid",
|
|
70
|
+
planner: "deep",
|
|
71
|
+
reviewer: "deep",
|
|
72
|
+
leader: "mid",
|
|
73
|
+
},
|
|
74
|
+
roleTiers: {
|
|
75
|
+
researcher: "cheap",
|
|
76
|
+
docs: "cheap",
|
|
77
|
+
backend: "mid",
|
|
78
|
+
frontend: "mid",
|
|
79
|
+
tester: "mid",
|
|
80
|
+
planner: "deep",
|
|
81
|
+
reviewer: "deep",
|
|
82
|
+
leader: "mid",
|
|
83
|
+
},
|
|
84
|
+
defaultTier: "mid",
|
|
85
|
+
defaultThinkingLevel: undefined,
|
|
86
|
+
tierThinkingLevels: {
|
|
87
|
+
...(DEFAULT_MODEL_CONFIG.tierThinkingLevels ?? {}),
|
|
88
|
+
cheap: "minimal",
|
|
89
|
+
mid: "medium",
|
|
90
|
+
deep: "high",
|
|
91
|
+
},
|
|
92
|
+
roleThinkingLevels: {},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe("isModelTier", () => {
|
|
96
|
+
test("accepts the three tiers", () => {
|
|
97
|
+
assert.equal(isModelTier("cheap"), true);
|
|
98
|
+
assert.equal(isModelTier("mid"), true);
|
|
99
|
+
assert.equal(isModelTier("deep"), true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("rejects anything else", () => {
|
|
103
|
+
assert.equal(isModelTier("MID"), false);
|
|
104
|
+
assert.equal(isModelTier("fast"), false);
|
|
105
|
+
assert.equal(isModelTier(""), false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("loadModelConfig / saveModelConfig", () => {
|
|
110
|
+
test("round-trips a config to disk", async () => {
|
|
111
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-cfg-"));
|
|
112
|
+
try {
|
|
113
|
+
await saveModelConfig(USER_CONFIG, dir);
|
|
114
|
+
const loaded = await loadModelConfig(dir);
|
|
115
|
+
assert.deepEqual(loaded, USER_CONFIG);
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(dir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("returns defaults when config file is missing", async () => {
|
|
122
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-cfg-"));
|
|
123
|
+
try {
|
|
124
|
+
const loaded = await loadModelConfig(dir);
|
|
125
|
+
assert.deepEqual(loaded, DEFAULT_MODEL_CONFIG);
|
|
126
|
+
} finally {
|
|
127
|
+
await rm(dir, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("merges partial config with defaults", async () => {
|
|
132
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-cfg-"));
|
|
133
|
+
try {
|
|
134
|
+
await writeFile(
|
|
135
|
+
modelConfigPath(dir),
|
|
136
|
+
JSON.stringify({ provider: "openai-codex", defaultTier: "cheap" }),
|
|
137
|
+
"utf8",
|
|
138
|
+
);
|
|
139
|
+
const loaded = await loadModelConfig(dir);
|
|
140
|
+
assert.equal(loaded.provider, "openai-codex");
|
|
141
|
+
assert.equal(loaded.defaultTier, "cheap");
|
|
142
|
+
// defaults are still there
|
|
143
|
+
assert.equal(loaded.roleTiers.researcher, "cheap");
|
|
144
|
+
assert.ok(loaded.providers.anthropic);
|
|
145
|
+
} finally {
|
|
146
|
+
await rm(dir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("accepts compact tiers/roles config", async () => {
|
|
151
|
+
const dir = await mkdtemp(join(tmpdir(), "team-mode-cfg-"));
|
|
152
|
+
try {
|
|
153
|
+
await writeFile(
|
|
154
|
+
modelConfigPath(dir),
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
provider: "openai-codex",
|
|
157
|
+
defaultTier: "md",
|
|
158
|
+
tiers: {
|
|
159
|
+
sm: { name: "Small", thinkingLevel: "low" },
|
|
160
|
+
md: { name: "Medium", thinkingLevel: "medium" },
|
|
161
|
+
lg: { name: "Large", thinkingLevel: "high" },
|
|
162
|
+
},
|
|
163
|
+
roles: {
|
|
164
|
+
researcher: "sm",
|
|
165
|
+
backend: "md",
|
|
166
|
+
planner: "lg",
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
"utf8",
|
|
170
|
+
);
|
|
171
|
+
const loaded = await loadModelConfig(dir);
|
|
172
|
+
assert.equal(loaded.roles.researcher, "sm");
|
|
173
|
+
assert.equal(loaded.tiers.sm?.thinkingLevel, "low");
|
|
174
|
+
const resolved = resolveModel(loaded, "planner");
|
|
175
|
+
assert.ok(resolved);
|
|
176
|
+
assert.equal(resolved.tier, "lg");
|
|
177
|
+
assert.equal(resolved.model, "gpt-5.4");
|
|
178
|
+
assert.equal(resolved.thinkingLevel, "high");
|
|
179
|
+
} finally {
|
|
180
|
+
await rm(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("resolveModel — user's real config", () => {
|
|
186
|
+
test("researcher → openai-codex/gpt-5.4-mini", () => {
|
|
187
|
+
const resolved = resolveModel(USER_CONFIG, "researcher");
|
|
188
|
+
assert.ok(resolved);
|
|
189
|
+
assert.equal(resolved.provider, "openai-codex");
|
|
190
|
+
assert.equal(resolved.model, "gpt-5.4-mini");
|
|
191
|
+
assert.equal(resolved.tier, "cheap");
|
|
192
|
+
assert.match(resolved.rationale, /researcher/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("backend → openai-codex/gpt-5.4", () => {
|
|
196
|
+
const resolved = resolveModel(USER_CONFIG, "backend");
|
|
197
|
+
assert.ok(resolved);
|
|
198
|
+
assert.equal(resolved.model, "gpt-5.4");
|
|
199
|
+
assert.equal(resolved.tier, "mid");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("reviewer → openai-codex/gpt-5.4 with high thinking (deep)", () => {
|
|
203
|
+
const resolved = resolveModel(USER_CONFIG, "reviewer");
|
|
204
|
+
assert.ok(resolved);
|
|
205
|
+
assert.equal(resolved.model, "gpt-5.4");
|
|
206
|
+
assert.equal(resolved.tier, "deep");
|
|
207
|
+
assert.equal(resolved.thinkingLevel, "high");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("roleThinkingLevels overrides tier defaults", () => {
|
|
211
|
+
const resolved = resolveModel(
|
|
212
|
+
{
|
|
213
|
+
...USER_CONFIG,
|
|
214
|
+
roleThinkingLevels: { reviewer: "xhigh" },
|
|
215
|
+
},
|
|
216
|
+
"reviewer",
|
|
217
|
+
);
|
|
218
|
+
assert.ok(resolved);
|
|
219
|
+
assert.equal(resolved.model, "gpt-5.4");
|
|
220
|
+
assert.equal(resolved.thinkingLevel, "xhigh");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("unknown role falls back to defaultTier (mid)", () => {
|
|
224
|
+
const resolved = resolveModel(USER_CONFIG, "unknown-role");
|
|
225
|
+
assert.ok(resolved);
|
|
226
|
+
assert.equal(resolved.tier, "mid");
|
|
227
|
+
assert.match(resolved.rationale, /default tier/);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("tierOverride wins over role", () => {
|
|
231
|
+
const resolved = resolveModel(USER_CONFIG, "reviewer", "cheap");
|
|
232
|
+
assert.ok(resolved);
|
|
233
|
+
assert.equal(resolved.tier, "cheap");
|
|
234
|
+
assert.equal(resolved.model, "gpt-5.4-mini");
|
|
235
|
+
assert.match(resolved.rationale, /override/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("returns null when resolved provider has no catalog", () => {
|
|
239
|
+
const noOpenAI: ModelConfig = {
|
|
240
|
+
...USER_CONFIG,
|
|
241
|
+
provider: "missing-provider",
|
|
242
|
+
};
|
|
243
|
+
assert.equal(resolveModel(noOpenAI, "backend"), null);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("detectProvider", () => {
|
|
248
|
+
test("explicit non-auto wins", () => {
|
|
249
|
+
assert.equal(detectProvider("anthropic"), "anthropic");
|
|
250
|
+
assert.equal(detectProvider("openai-codex"), "openai-codex");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("auto consults PI_TEAM_MATE_MODEL_PROVIDER env", () => {
|
|
254
|
+
withEnv({ PI_TEAM_MATE_MODEL_PROVIDER: "openai-codex" }, () => {
|
|
255
|
+
assert.equal(detectProvider("auto"), "openai-codex");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("auto falls through to anthropic when nothing is configured", () => {
|
|
260
|
+
withEnv(
|
|
261
|
+
{
|
|
262
|
+
PI_TEAM_MATE_MODEL_PROVIDER: undefined,
|
|
263
|
+
PI_CODING_AGENT_DIR: "/tmp/nonexistent-dir-for-test",
|
|
264
|
+
ANTHROPIC_API_KEY: undefined,
|
|
265
|
+
OPENAI_API_KEY: undefined,
|
|
266
|
+
},
|
|
267
|
+
() => {
|
|
268
|
+
assert.equal(detectProvider("auto"), "anthropic");
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { renderTaskNotification } from "../ui/notification-box.ts";
|
|
5
|
+
|
|
6
|
+
describe("renderTaskNotification", () => {
|
|
7
|
+
const theme = {
|
|
8
|
+
fg: (_c: string, text: string) => text,
|
|
9
|
+
bg: (_c: string, text: string) => text,
|
|
10
|
+
bold: (text: string) => text,
|
|
11
|
+
} as never;
|
|
12
|
+
|
|
13
|
+
test("returns undefined when details are missing", () => {
|
|
14
|
+
const out = renderTaskNotification({ content: "x" }, { expanded: false }, theme);
|
|
15
|
+
assert.equal(out, undefined);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("returns a component when details exist", () => {
|
|
19
|
+
const out = renderTaskNotification(
|
|
20
|
+
{
|
|
21
|
+
content: "Result text",
|
|
22
|
+
details: {
|
|
23
|
+
taskId: "agent-1",
|
|
24
|
+
status: "completed",
|
|
25
|
+
durationMs: 1500,
|
|
26
|
+
summary: "done",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{ expanded: false },
|
|
30
|
+
theme,
|
|
31
|
+
);
|
|
32
|
+
assert.ok(out);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { aggregateParallelOutputs, mapConcurrent } from "../core/parallel-utils.ts";
|
|
5
|
+
|
|
6
|
+
describe("mapConcurrent", () => {
|
|
7
|
+
test("keeps result order while limiting concurrency", async () => {
|
|
8
|
+
let running = 0;
|
|
9
|
+
let maxRunning = 0;
|
|
10
|
+
const out = await mapConcurrent([30, 10, 20], 2, async (ms, idx) => {
|
|
11
|
+
running += 1;
|
|
12
|
+
maxRunning = Math.max(maxRunning, running);
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
running -= 1;
|
|
15
|
+
return `#${idx}`;
|
|
16
|
+
});
|
|
17
|
+
assert.deepEqual(out, ["#0", "#1", "#2"]);
|
|
18
|
+
assert.ok(maxRunning <= 2);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("aggregateParallelOutputs", () => {
|
|
23
|
+
test("renders per-task sections", () => {
|
|
24
|
+
const text = aggregateParallelOutputs([
|
|
25
|
+
{ name: "one", output: "A", exitCode: 0 },
|
|
26
|
+
{ name: "two", output: "B", exitCode: 1, error: "boom" },
|
|
27
|
+
]);
|
|
28
|
+
assert.match(text, /Parallel Task 1 \(one\)/);
|
|
29
|
+
assert.match(text, /Parallel Task 2 \(two\)/);
|
|
30
|
+
assert.match(text, /error: boom/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { PiStreamParser } from "../runtime/pi-stream-parser.ts";
|
|
5
|
+
|
|
6
|
+
describe("PiStreamParser", () => {
|
|
7
|
+
test("handles split JSON chunks", () => {
|
|
8
|
+
const parser = new PiStreamParser();
|
|
9
|
+
const first = parser.push('{"type":"message_update","assistantMessageEvent":{"type":"text_delta","text":"Hel');
|
|
10
|
+
assert.equal(first.length, 0);
|
|
11
|
+
const second = parser.push('lo"}}\n');
|
|
12
|
+
assert.deepEqual(second, [{ type: "assistant_delta", text: "Hello" }]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("ignores non-json lines", () => {
|
|
16
|
+
const parser = new PiStreamParser();
|
|
17
|
+
const events = parser.push("plain text\n{\"type\":\"turn_end\"}\n");
|
|
18
|
+
assert.deepEqual(events, [{ type: "turn_end" }]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("maps assistant message usage + tool events", () => {
|
|
22
|
+
const parser = new PiStreamParser();
|
|
23
|
+
const input = [
|
|
24
|
+
JSON.stringify({ type: "tool_execution_start", toolName: "Read", args: { path: "a.ts" } }),
|
|
25
|
+
JSON.stringify({ type: "tool_execution_end", toolName: "Read", result: "ok" }),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
type: "message_end",
|
|
28
|
+
message: {
|
|
29
|
+
role: "assistant",
|
|
30
|
+
content: [{ type: "text", text: "Done" }],
|
|
31
|
+
usage: { totalTokens: 42 },
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
].join("\n");
|
|
35
|
+
const events = parser.push(`${input}\n`);
|
|
36
|
+
assert.equal(events[0]?.type, "tool_start");
|
|
37
|
+
assert.equal(events[1]?.type, "tool_end");
|
|
38
|
+
assert.equal(events[2]?.type, "assistant_message");
|
|
39
|
+
if (events[2]?.type !== "assistant_message") return;
|
|
40
|
+
assert.equal(events[2].text, "Done");
|
|
41
|
+
assert.equal(events[2].usage?.totalTokens, 42);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("maps tool-only assistant messages so tool-use turns count", () => {
|
|
45
|
+
const parser = new PiStreamParser();
|
|
46
|
+
const events = parser.push(
|
|
47
|
+
`${JSON.stringify({
|
|
48
|
+
type: "message_end",
|
|
49
|
+
message: {
|
|
50
|
+
role: "assistant",
|
|
51
|
+
content: [{ type: "toolCall", id: "call_1", name: "read", arguments: { path: "a.ts" } }],
|
|
52
|
+
usage: { input: 10, output: 2, cacheRead: 30, cacheWrite: 0 },
|
|
53
|
+
},
|
|
54
|
+
})}\n`,
|
|
55
|
+
);
|
|
56
|
+
assert.equal(events.length, 1);
|
|
57
|
+
assert.equal(events[0]?.type, "assistant_message");
|
|
58
|
+
if (events[0]?.type !== "assistant_message") return;
|
|
59
|
+
assert.equal(events[0].text, "");
|
|
60
|
+
assert.equal(events[0].usage?.inputTokens, 10);
|
|
61
|
+
assert.equal(events[0].usage?.cacheReadTokens, 30);
|
|
62
|
+
assert.equal(events[0].usage?.totalTokens, 42);
|
|
63
|
+
});
|
|
64
|
+
});
|