@maxiaochao/pi-toolkit 0.13.5 → 0.13.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { test } from "node:test";
8
+ import { piBinary } from "./runtime.mjs";
9
+
10
+ test("native Codemode receives start guidance and exposes it to the model", () => {
11
+ const directory = mkdtempSync(join(tmpdir(), "pi-bg-guidance-"));
12
+ try {
13
+ const events = join(directory, "events.jsonl");
14
+ const output = execFileSync(piBinary, [
15
+ "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-session", "--offline",
16
+ "-e", "builtin:codemode", "-e", fileURLToPath(new URL("../extensions/background-tasks.ts", import.meta.url)),
17
+ "-e", fileURLToPath(new URL("fixtures/provider.ts", import.meta.url)),
18
+ "--tools", "codemode,background_task", "--model", "bg-fixture/deterministic", "-p", "SMOKE_GUIDANCE",
19
+ ], { cwd: directory, encoding: "utf8", timeout: 30000,
20
+ env: { ...process.env, PI_CODING_AGENT_DIR: directory, PI_OFFLINE: "1", BG_SMOKE_EVENTS: events }, stdio: ["ignore", "pipe", "pipe"] });
21
+ assert.match(output, /Main agent is ready/);
22
+ const records = readFileSync(events, "utf8").trim().split("\n").map((line) => JSON.parse(line));
23
+ const started = records.find((entry) => entry.event === "tool_end" && entry.name === "background_task");
24
+ assert.equal(started.isError, false);
25
+ const guidance = started.result.structuredContent.guidance;
26
+ assert.equal(guidance, "完成会自动通知。现在不要查询等待;继续其他独立工作,没有其他工作就结束当前轮,收到通知后再取结果。");
27
+ const codemode = records.find((entry) => entry.event === "tool_end" && entry.name === "codemode");
28
+ assert.equal(codemode.isError, false);
29
+ assert.ok(codemode.result.content.some((block) => block.type === "text" && block.text.includes(guidance)));
30
+ const requests = records.filter((entry) => entry.event === "request");
31
+ assert.ok(requests[0].backgroundDescription.includes(guidance));
32
+ assert.ok(requests[1].text.includes(guidance), "the next model request sees guidance printed from the programmatic result");
33
+ } finally { rmSync(directory, { recursive: true, force: true }); }
34
+ });
35
+
36
+ for (const mode of ["codemode", "direct"]) {
37
+ for (const reads of [0, 1, 4]) {
38
+ test(`real Pi ${mode} merges four completions before the next model call, excluding ${reads} consumed results`, () => {
39
+ const directory = mkdtempSync(join(tmpdir(), "pi-bg-notifications-"));
40
+ try {
41
+ const events = join(directory, "events.jsonl");
42
+ writeFileSync(join(directory, "settings.json"), JSON.stringify({ quietStartup: true, followUpMode: "one-at-a-time", steeringMode: "one-at-a-time" }));
43
+ const output = execFileSync(piBinary, [
44
+ "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-session", "--offline",
45
+ "-e", "builtin:codemode", "-e", process.env.BG_TEST_EXTENSION ?? fileURLToPath(new URL("../extensions/background-tasks.ts", import.meta.url)),
46
+ "-e", fileURLToPath(new URL("fixtures/notifications.ts", import.meta.url)),
47
+ "--tools", "codemode,background_task,synchronous_subagent_fixture", "--model", "bg-notifications/deterministic", "--mode", "json", "-p", "Run the synchronous subagent scenario",
48
+ ], { cwd: directory, encoding: "utf8", timeout: 30000, env: { ...process.env, PI_CODING_AGENT_DIR: directory, PI_OFFLINE: "1", BG_NOTIFICATION_EVENTS: events, BG_NOTIFICATION_READS: String(reads), BG_NOTIFICATION_MODE: mode }, stdio: ["ignore", "pipe", "pipe"] });
49
+ assert.doesNotMatch(output, /Failed to load|Error loading|Fixture shell jobs did not finish/);
50
+ const records = readFileSync(events, "utf8").trim().split("\n").map((line) => JSON.parse(line));
51
+ const returned = records.find((entry) => entry.event === "returned");
52
+ assert.ok(returned, `the blocking tool finished all real shell jobs\n${output}`);
53
+ assert.equal(returned.tasks.length, 4);
54
+ assert.ok(returned.tasks.every((task) => task.status === "failed" && task.exit_code === 7));
55
+ const transcript = output.trim().split("\n").map((line) => JSON.parse(line));
56
+ const notifications = transcript.filter((entry) =>
57
+ (entry.type === "entry_appended" && entry.entry.customType === "background-task-finished") ||
58
+ (entry.type === "message_start" && entry.message.customType === "background-task-finished")
59
+ );
60
+ assert.equal(notifications.length, reads === 4 ? 0 : 1);
61
+ if (reads !== 4) {
62
+ const message = notifications[0].entry ?? notifications[0].message;
63
+ const tasks = message.details.tasks;
64
+ assert.equal(tasks.length, 4 - reads);
65
+ assert.ok(tasks.every((task) => !returned.consumed.includes(task.id)));
66
+ const toolEnd = transcript.findIndex((entry) => entry.type === "tool_execution_end" && entry.toolName === "synchronous_subagent_fixture");
67
+ assert.ok(toolEnd >= 0 && transcript.indexOf(notifications[0]) > toolEnd, "the notification cannot interrupt the synchronous tool");
68
+ assert.match(message.content, /Ended at: \d{4}-/);
69
+ }
70
+ const requests = records.filter((entry) => entry.event === "request");
71
+ assert.equal(requests.length, mode === "direct" ? 3 : 2, "no extra model turns after the final report");
72
+ const nextContext = JSON.stringify(requests.at(-1).messages);
73
+ assert.equal((nextContext.match(/\[Background task finished\]/g) ?? []).length, reads === 4 ? 0 : 1);
74
+ assert.doesNotMatch(nextContext, /FINAL_REPORT/, "completion is delivered before, not after, the final report");
75
+ const contextText = requests.at(-1).messages.flatMap((message) => typeof message.content === "string" ? [message.content] : (message.content ?? []).filter((block) => block.type === "text").map((block) => block.text)).join("\n");
76
+ for (const task of returned.tasks.filter((task) => !returned.consumed.includes(task.id))) assert.ok(contextText.includes(task.id));
77
+ } finally { rmSync(directory, { recursive: true, force: true }); }
78
+ });
79
+ }
80
+ }
@@ -138,12 +138,15 @@ try:
138
138
  shot('running')
139
139
  send('/bg output ' + first['id'])
140
140
  send('SMOKE_CANCEL')
141
- wait(lambda: bool(finished('cancelled')), 'cancel follow-up')
142
- wait(lambda: bool(finished('succeeded')), 'successful background follow-up')
141
+ wait(lambda: bool(finished('cancelled')), 'cancel notification')
142
+ wait(lambda: bool(finished('succeeded')), 'successful background notification')
143
143
  shot('completed')
144
144
  send('SMOKE_OUTPUT')
145
145
  wait(lambda: any(e.get('result', {}).get('structuredContent', {}).get('output') == 'BUILD SUCCESSFUL\n' for e in tools()), 'explicit output query')
146
- wait(lambda: 'BACKGROUND_FOLLOWUP_RECEIVED' in raw, 'main agent automatic response')
146
+ wait(lambda: 'BACKGROUND_NOTIFICATION_RECEIVED' in raw, 'main agent automatic response')
147
+ pump(0.5)
148
+ notifications = [e for e in events() if e.get('event') == 'custom' and e.get('type') == 'background-task-finished']
149
+ assert len(notifications) == 2, 'Duplicate persisted busy/idle notifications'
147
150
  pump(5.3)
148
151
  shot('history')
149
152
 
@@ -174,7 +177,8 @@ try:
174
177
  (OUT / 'events.jsonl').write_text('\n'.join(json.dumps(e) for e in records) + '\n')
175
178
  (OUT / 'result.json').write_text(json.dumps({'passed': True, 'screenshots': screens,
176
179
  'checks': ['two jobs', 'third rejected', 'Codemode returns while jobs live', 'Scheduler coexistence', '/bg output',
177
- 'cancel notification', 'success notification', 'main agent follow-up', '5-second terminal row', 'reload cleanup', 'new-session cleanup', 'quit cleanup']}, indent=2))
180
+ 'cancel notification', 'success notification', 'main agent notification', 'no duplicate persisted notifications',
181
+ '5-second terminal row', 'reload cleanup', 'new-session cleanup', 'quit cleanup']}, indent=2))
178
182
  except Exception:
179
183
  (OUT / 'error.txt').write_text(traceback.format_exc())
180
184
  raise