@preapexis/piexis 0.2.4 → 0.2.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.
- package/README.md +22 -2
- package/apps/server/src/index.ts +97 -0
- package/apps/server/src/session-manager.ts +95 -0
- package/apps/server/src/workspace.ts +39 -0
- package/{tsconfig.json → apps/server/tsconfig.json} +3 -4
- package/apps/web/dist/assets/index-D4n3_lAw.js +10 -0
- package/apps/web/dist/assets/index-igiK0FPY.css +1 -0
- package/apps/web/dist/index.html +3 -0
- package/bin/piexis-web.mjs +24 -0
- package/extensions/webui.ts +45 -0
- package/package.json +38 -3
- package/packages/pi-integration/src/rpc-client.ts +104 -0
- package/packages/shared/src/index.ts +27 -0
- package/themes/midnight.json +81 -0
- package/.github/workflows/publish.yml +0 -35
- package/AGENTS.md +0 -38
- package/scripts/release.mjs +0 -32
- package/skills-lock.json +0 -365
- package/tests/agents.test.mjs +0 -605
- package/tests/ask-question-pty.test.mjs +0 -16
- package/tests/ask-question.test.mjs +0 -269
- package/tests/fixtures/agents-provider.ts +0 -55
- package/tests/fixtures/ask-question-pty.py +0 -91
- package/tests/fixtures/ask-question-terminal.mjs +0 -69
- package/tests/fixtures/recommended-tools-terminal.mjs +0 -103
- package/tests/init-workflow.test.mjs +0 -31
- package/tests/recommended-tools-pty.test.mjs +0 -16
- package/tests/recommended-tools.test.mjs +0 -423
- package/tests/resources.test.mjs +0 -66
- package/tests/skills-routing.test.mjs +0 -54
- package/tests/ui-tip-reload.test.mjs +0 -70
- package/tests/ui.test.mjs +0 -450
package/tests/agents.test.mjs
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import test from "node:test";
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import agentsExtension from "../extensions/agents.ts";
|
|
10
|
-
import uiExtension from "../extensions/ui.ts";
|
|
11
|
-
import { matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
|
-
import { agentNames, childResourceArgs, initializeAgentConfig, isSmallTask, loadAgentConfig, parseAgentConfig, routeSubagent, subagentTools } from "../lib/agents.ts";
|
|
13
|
-
import { agentConfigSchemaUrl } from "../lib/agent-config.ts";
|
|
14
|
-
import { emptyUsage, limitOutput, piInvocation, runSubagent } from "../lib/subagent-runner.ts";
|
|
15
|
-
|
|
16
|
-
async function fixture(work) {
|
|
17
|
-
const root = await mkdtemp(join(tmpdir(), "piexis-agents-"));
|
|
18
|
-
const cwd = join(root, "project"), agentDir = join(root, "agent");
|
|
19
|
-
await mkdir(join(cwd, ".pi"), { recursive: true });
|
|
20
|
-
await mkdir(agentDir);
|
|
21
|
-
const cli = join(root, "cli.mjs");
|
|
22
|
-
await writeFile(cli, `
|
|
23
|
-
let task = "";
|
|
24
|
-
for await (const chunk of process.stdin) task += chunk;
|
|
25
|
-
const usage = { input: 10, output: 5, cacheRead: 2, cacheWrite: 1, totalTokens: 18,
|
|
26
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.01 } };
|
|
27
|
-
const message = { role: "assistant", content: [{ type: "text", text: JSON.stringify({ task, args: process.argv.slice(2), depth: process.env.PIEXIS_SUBAGENT_DEPTH }) }], usage, stopReason: "stop" };
|
|
28
|
-
process.stdout.write(JSON.stringify({ type: "message_end", message }) + "\\n");
|
|
29
|
-
`);
|
|
30
|
-
try { await work({ root, cwd, agentDir, cli }); }
|
|
31
|
-
finally { await rm(root, { recursive: true, force: true }); }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function environment(f, work) {
|
|
35
|
-
const argv = process.argv, dir = process.env.PI_CODING_AGENT_DIR, depth = process.env.PIEXIS_SUBAGENT_DEPTH;
|
|
36
|
-
process.argv = [process.execPath, f.cli];
|
|
37
|
-
process.env.PI_CODING_AGENT_DIR = f.agentDir;
|
|
38
|
-
delete process.env.PIEXIS_SUBAGENT_DEPTH;
|
|
39
|
-
try { await work(); }
|
|
40
|
-
finally {
|
|
41
|
-
process.argv = argv;
|
|
42
|
-
for (const [key, value] of [["PI_CODING_AGENT_DIR", dir], ["PIEXIS_SUBAGENT_DEPTH", depth]]) {
|
|
43
|
-
if (value === undefined) delete process.env[key]; else process.env[key] = value;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function harness(f, flag) {
|
|
49
|
-
const handlers = new Map(), commands = new Map(), shortcuts = new Map(), statuses = new Map(), notifications = [], states = [];
|
|
50
|
-
let tool, active = ["read", "bash", "edit", "write", "grep", "find", "ls", "AskQuestion", "subagent"], mainModel, effort;
|
|
51
|
-
const pi = {
|
|
52
|
-
on(name, handler) { handlers.set(name, handler); }, registerFlag() {}, getFlag: () => flag,
|
|
53
|
-
registerCommand(name, command) { commands.set(name, command); }, registerTool(value) { tool = value; },
|
|
54
|
-
registerShortcut(key, shortcut) { shortcuts.set(key, shortcut); },
|
|
55
|
-
getActiveTools: () => active, setActiveTools(value) { active = value; },
|
|
56
|
-
getThinkingLevel: () => "high", appendEntry(type, data) { states.push({ type: "custom", customType: type, data }); },
|
|
57
|
-
async setModel(model) {
|
|
58
|
-
if (model.id === "unauthenticated") return false;
|
|
59
|
-
mainModel = model; return true;
|
|
60
|
-
},
|
|
61
|
-
setThinkingLevel(value) { effort = value; },
|
|
62
|
-
};
|
|
63
|
-
agentsExtension(pi);
|
|
64
|
-
const ctx = {
|
|
65
|
-
cwd: f.cwd, hasUI: true, mode: "json", model: { provider: "test", id: "current" }, thinkingLevel: "high",
|
|
66
|
-
isProjectTrusted: () => true, isIdle: () => true, waitForIdle: async () => {},
|
|
67
|
-
sessionManager: { getBranch: () => states, getEntries: () => states },
|
|
68
|
-
modelRegistry: { find: (provider, id) => provider === "test" && id !== "missing" ? { provider, id } : undefined,
|
|
69
|
-
hasConfiguredAuth: (model) => model.id !== "unauthenticated" },
|
|
70
|
-
ui: { setStatus(key, value) { statuses.set(key, value); }, notify(...args) { notifications.push(args); }, select: async () => undefined },
|
|
71
|
-
};
|
|
72
|
-
return { ctx, handlers, commands, shortcuts, statuses, notifications, states, get tool() { return tool; },
|
|
73
|
-
get active() { return active; }, get mainModel() { return mainModel; }, get effort() { return effort; },
|
|
74
|
-
start: () => handlers.get("session_start")({}, ctx),
|
|
75
|
-
execute: (params, signal, update) => tool.execute("call", params, signal, update, ctx),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const current = "test/current";
|
|
80
|
-
test("validates configuration and rejects typos, bad models and efforts", () => {
|
|
81
|
-
assert.deepEqual(parseAgentConfig({}), {});
|
|
82
|
-
for (const bad of [null, [], { model: "bare-id" }, { model: "test/has space" }, { smallModel: "test/model" },
|
|
83
|
-
{ subagent_variant_overrides: [] }, { subagent_variant_overrides: { worker: {} } },
|
|
84
|
-
{ subagent_variant_overrides: { code: { effort: "huge" } } }, { subagent_variant_overrides: { code: { tools: [] } } }]) {
|
|
85
|
-
assert.throws(() => parseAgentConfig(bad));
|
|
86
|
-
}
|
|
87
|
-
assert.equal(parseAgentConfig({ subagent_model: "provider/org/model", delegate_exploration: true }).delegate_exploration, true);
|
|
88
|
-
assert.throws(() => parseAgentConfig({ delegate_exploration: "true" }));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("trusted project config merges fields within overrides without writing settings", async () => {
|
|
92
|
-
await fixture(async (f) => {
|
|
93
|
-
const global = { model: "test/main", small_model: "test/small", subagent_variant_overrides: { code: { model: "test/code", effort: "high" }, debug: { effort: "high" } } };
|
|
94
|
-
const local = { subagent_model: "test/worker", subagent_variant_overrides: { code: { effort: "low" } } };
|
|
95
|
-
await writeFile(join(f.agentDir, "agents.json"), JSON.stringify(global));
|
|
96
|
-
await writeFile(join(f.cwd, ".pi", "agents.json"), JSON.stringify(local));
|
|
97
|
-
const config = await loadAgentConfig(f.cwd, f.agentDir, true);
|
|
98
|
-
assert.deepEqual(config.subagent_variant_overrides.code, { model: "test/code", effort: "low" });
|
|
99
|
-
assert.deepEqual(config.subagent_variant_overrides.debug, { effort: "high" });
|
|
100
|
-
assert.equal(config.small_model, "test/small");
|
|
101
|
-
assert.equal(config.subagent_model, "test/worker");
|
|
102
|
-
assert.deepEqual(await loadAgentConfig(f.cwd, f.agentDir, false), global);
|
|
103
|
-
await writeFile(join(f.cwd, ".pi", "agents.json"), "{bad");
|
|
104
|
-
await assert.rejects(loadAgentConfig(f.cwd, f.agentDir, true), /agents.json/);
|
|
105
|
-
assert.deepEqual(await loadAgentConfig(f.cwd, f.agentDir, false), global);
|
|
106
|
-
await assert.rejects(readFile(join(f.cwd, ".pi", "settings.json")), { code: "ENOENT" });
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("singular agent.json is preferred per scope, with legacy agents.json fallback", async () => {
|
|
111
|
-
await fixture(async (f) => {
|
|
112
|
-
await writeFile(join(f.agentDir, "agents.json"), '{"model":"test/legacy"}');
|
|
113
|
-
await writeFile(join(f.agentDir, "agent.json"), '{"model":"test/main"}');
|
|
114
|
-
await writeFile(join(f.cwd, ".pi", "agents.json"), '{"subagent_model":"test/worker"}');
|
|
115
|
-
assert.equal((await loadAgentConfig(f.cwd, f.agentDir, true)).model, "test/main");
|
|
116
|
-
assert.equal((await loadAgentConfig(f.cwd, f.agentDir, true)).subagent_model, "test/worker");
|
|
117
|
-
await writeFile(join(f.cwd, ".pi", "agent.json"), '{"subagent_model":"test/local"}');
|
|
118
|
-
assert.equal((await loadAgentConfig(f.cwd, f.agentDir, true)).subagent_model, "test/local");
|
|
119
|
-
assert.equal((await loadAgentConfig(f.cwd, f.agentDir, false)).subagent_model, undefined);
|
|
120
|
-
await writeFile(join(f.cwd, ".pi", "agent.json"), "{bad");
|
|
121
|
-
await assert.rejects(loadAgentConfig(f.cwd, f.agentDir, true), /agent.json/);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("opt-in agent scaffold uses the active model, restrictive permissions and atomic create-only writes", async () => {
|
|
126
|
-
await fixture(async (f) => {
|
|
127
|
-
const created = await Promise.all([initializeAgentConfig(f.cwd, f.agentDir, "test/current"), initializeAgentConfig(f.cwd, f.agentDir, "test/other")]);
|
|
128
|
-
assert.deepEqual(created, [true, false]);
|
|
129
|
-
const path = join(f.cwd, ".pi", "agent.json");
|
|
130
|
-
assert.deepEqual(JSON.parse(await readFile(path, "utf8")), {
|
|
131
|
-
$schema: agentConfigSchemaUrl, model: "test/current", small_model: "test/current", subagent_model: "test/current", delegate_exploration: false,
|
|
132
|
-
subagent_variant_overrides: {
|
|
133
|
-
plan: { model: "test/current", effort: "high" },
|
|
134
|
-
orchestrator: { model: "test/current", effort: "high" },
|
|
135
|
-
debug: { model: "test/current", effort: "high" },
|
|
136
|
-
ask: { model: "test/current", effort: "low" },
|
|
137
|
-
code: { model: "test/current", effort: "medium" },
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
assert.equal((await stat(path)).mode & 0o777, 0o600);
|
|
141
|
-
assert.deepEqual(await readdir(join(f.cwd, ".pi")), ["agent.json"]);
|
|
142
|
-
assert.equal((await loadAgentConfig(f.cwd, f.agentDir, true)).model, "test/current");
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("agent-config command fills only missing project settings", async () => {
|
|
147
|
-
await fixture(async (f) => environment(f, async () => {
|
|
148
|
-
await writeFile(join(f.cwd, ".pi", "agent.json"), JSON.stringify({ model: "test/custom", delegate_exploration: true }));
|
|
149
|
-
const h = harness(f);
|
|
150
|
-
await h.commands.get("agent-config").handler("", h.ctx);
|
|
151
|
-
const config = JSON.parse(await readFile(join(f.cwd, ".pi", "agent.json"), "utf8"));
|
|
152
|
-
assert.equal(config.$schema, agentConfigSchemaUrl);
|
|
153
|
-
assert.equal(config.model, "test/custom");
|
|
154
|
-
assert.equal(config.delegate_exploration, true);
|
|
155
|
-
assert.equal(config.small_model, "test/custom");
|
|
156
|
-
assert.equal(config.subagent_model, "test/custom");
|
|
157
|
-
assert.equal(config.subagent_variant_overrides.code.model, "test/custom");
|
|
158
|
-
assert.match(h.notifications.at(-1)[0], /Added missing/);
|
|
159
|
-
await h.commands.get("agent-config").handler("", h.ctx);
|
|
160
|
-
assert.match(h.notifications.at(-1)[0], /already complete/);
|
|
161
|
-
}));
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
test("agent scaffold preserves inherited user configuration", async () => {
|
|
165
|
-
await fixture(async (f) => {
|
|
166
|
-
const inherited = { model: "test/main", small_model: "test/small", subagent_model: "test/worker", delegate_exploration: true, subagent_variant_overrides: {
|
|
167
|
-
debug: { effort: "off" }, ask: { model: "test/answer" }, code: { model: "test/code", effort: "max" }
|
|
168
|
-
} };
|
|
169
|
-
const path = join(f.agentDir, "agent.json");
|
|
170
|
-
const original = JSON.stringify(inherited);
|
|
171
|
-
await writeFile(path, original);
|
|
172
|
-
assert.equal(await initializeAgentConfig(f.cwd, f.agentDir, "test/current"), true);
|
|
173
|
-
assert.deepEqual(JSON.parse(await readFile(join(f.cwd, ".pi", "agent.json"), "utf8")), {
|
|
174
|
-
$schema: agentConfigSchemaUrl,
|
|
175
|
-
...inherited,
|
|
176
|
-
subagent_variant_overrides: {
|
|
177
|
-
plan: { model: "test/worker", effort: "high" },
|
|
178
|
-
orchestrator: { model: "test/worker", effort: "high" },
|
|
179
|
-
debug: { model: "test/worker", effort: "off" },
|
|
180
|
-
ask: { model: "test/answer", effort: "low" },
|
|
181
|
-
code: { model: "test/code", effort: "max" },
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
assert.equal(await readFile(path, "utf8"), original);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("agent scaffold never replaces existing, malformed, legacy or non-regular files", async () => {
|
|
189
|
-
for (const name of ["agent.json", "agents.json"]) {
|
|
190
|
-
for (const value of ['{"model":"test/custom"}', "{bad", "directory", "symlink"]) {
|
|
191
|
-
await fixture(async (f) => {
|
|
192
|
-
const path = join(f.cwd, ".pi", name);
|
|
193
|
-
const target = join(f.root, "target.json");
|
|
194
|
-
if (value === "directory") await mkdir(path);
|
|
195
|
-
else if (value === "symlink") { await writeFile(target, "do not change"); await symlink(target, path); }
|
|
196
|
-
else await writeFile(path, value);
|
|
197
|
-
assert.equal(await initializeAgentConfig(f.cwd, f.agentDir, "test/current"), false);
|
|
198
|
-
if (value === "symlink") assert.equal(await readFile(target, "utf8"), "do not change");
|
|
199
|
-
else if (value !== "directory") assert.equal(await readFile(path, "utf8"), value);
|
|
200
|
-
assert.deepEqual(await readdir(join(f.cwd, ".pi")), [name]);
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("missing models, invalid inherited configuration and cancellation do not create an agent file", async () => {
|
|
207
|
-
await fixture(async (f) => {
|
|
208
|
-
await assert.rejects(initializeAgentConfig(f.cwd, f.agentDir, undefined), /No active model/);
|
|
209
|
-
await assert.rejects(initializeAgentConfig(f.cwd, f.agentDir, "bare-id"), /provider\/model/);
|
|
210
|
-
const controller = new AbortController(); controller.abort();
|
|
211
|
-
await assert.rejects(initializeAgentConfig(f.cwd, f.agentDir, "test/current", controller.signal), { name: "AbortError" });
|
|
212
|
-
await writeFile(join(f.agentDir, "agent.json"), "{bad");
|
|
213
|
-
await assert.rejects(initializeAgentConfig(f.cwd, f.agentDir, "test/current"), /agent.json/);
|
|
214
|
-
assert.deepEqual(await readdir(join(f.cwd, ".pi")), []);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("automatic small routing is conservative and variant overrides take precedence", () => {
|
|
219
|
-
const config = { model: "test/main", small_model: "test/small", subagent_model: "test/worker" };
|
|
220
|
-
for (const task of ["Locate the auth entry point", "What does parseConfig do?", "Summarize README.md"]) {
|
|
221
|
-
const route = routeSubagent(config, "ask", task, current, "high");
|
|
222
|
-
assert.deepEqual(route, { model: "test/small", effort: "low", reason: "automatic small task" });
|
|
223
|
-
}
|
|
224
|
-
assert.equal(isSmallTask("code", "Fix a typo in README.md"), true);
|
|
225
|
-
for (const [agent, task] of [["ask", "Explain the security architecture"], ["ask", "Summarize the entire repo"], ["code", "Implement login"], ["debug", "Fix typo"], ["plan", "List files"], ["orchestrator", "Find config"], ["ask", "List " + "x".repeat(401)]]) {
|
|
226
|
-
assert.equal(routeSubagent(config, agent, task, current, "high").model, "test/worker");
|
|
227
|
-
}
|
|
228
|
-
assert.equal(routeSubagent({}, "code", "task", current, "high").model, current);
|
|
229
|
-
assert.equal(routeSubagent({ model: "test/main" }, "code", "task", current, "high").model, current);
|
|
230
|
-
const overrides = { ...config, subagent_variant_overrides: { ask: { model: "test/ask", effort: "max" }, code: { effort: "high" } } };
|
|
231
|
-
assert.deepEqual(routeSubagent(overrides, "ask", "Locate config", current, "off"), { model: "test/ask", effort: "max", reason: "variant override" });
|
|
232
|
-
assert.equal(routeSubagent(overrides, "code", "Fix a typo", current, "off").effort, "high");
|
|
233
|
-
assert.deepEqual(routeSubagent(overrides, "ask", "Explore the configuration", current, "high", true), {
|
|
234
|
-
model: "test/small", effort: "low", reason: "configured exploration small model",
|
|
235
|
-
});
|
|
236
|
-
assert.throws(() => routeSubagent({}, "ask", "Explore", current, "high", true), /requires small_model/);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("child tools never add inactive tools and resource flags preserve explicit extensions", () => {
|
|
240
|
-
const active = ["read", "bash", "write", "AskQuestion", "subagent", "custom"];
|
|
241
|
-
assert.deepEqual(subagentTools("ask", active, 1), ["read"]);
|
|
242
|
-
assert.deepEqual(subagentTools("plan", active, 1), ["read"]);
|
|
243
|
-
assert.deepEqual(subagentTools("plan", active, 1, true), ["read", "subagent"]);
|
|
244
|
-
assert.deepEqual(subagentTools("ask", active, 2, true), ["read"]);
|
|
245
|
-
assert.deepEqual(subagentTools("orchestrator", active, 1), ["read", "bash", "write", "subagent", "custom"]);
|
|
246
|
-
assert.deepEqual(subagentTools("code", active, 2), ["read", "bash", "write", "custom"]);
|
|
247
|
-
assert.deepEqual(childResourceArgs(["--model", "test/main", "--no-extensions", "-e", "gate.ts", "--no-mcp", "--api-key", "private", "--", "-e", "bad.ts"]), ["--no-extensions", "-e", "gate.ts", "--no-mcp"]);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("all five profiles execute as child agents with visible routing and preserved loadout", async () => {
|
|
251
|
-
await fixture(async (f) => environment(f, async () => {
|
|
252
|
-
await writeFile(join(f.agentDir, "agents.json"), JSON.stringify({ small_model: "test/small", subagent_model: "test/worker", subagent_variant_overrides: { plan: { model: "test/planner", effort: "max" } } }));
|
|
253
|
-
process.argv.push("--no-extensions", "-e", "gate.ts");
|
|
254
|
-
const h = harness(f);
|
|
255
|
-
await h.start();
|
|
256
|
-
assert.equal(h.tool.executionMode, "sequential");
|
|
257
|
-
for (const agent of agentNames) {
|
|
258
|
-
const updates = [];
|
|
259
|
-
const result = await h.execute({ agent, task: agent === "ask" ? "Locate the config" : "Complete this task" }, undefined, (value) => updates.push(value));
|
|
260
|
-
assert.equal(result.isError, false);
|
|
261
|
-
const output = JSON.parse(result.details.output);
|
|
262
|
-
assert.equal(output.depth, "1");
|
|
263
|
-
assert.match(output.task, /^Task:/);
|
|
264
|
-
assert.ok(output.args.includes("--approve"));
|
|
265
|
-
assert.ok(output.args.includes("gate.ts"));
|
|
266
|
-
assert.equal(output.args[output.args.indexOf("--agent") + 1], agent);
|
|
267
|
-
const tools = output.args[output.args.indexOf("--tools") + 1].split(",");
|
|
268
|
-
assert.ok(!tools.includes("AskQuestion"));
|
|
269
|
-
if (["plan", "ask"].includes(agent)) {
|
|
270
|
-
assert.ok(!tools.includes("bash") && !tools.includes("subagent"));
|
|
271
|
-
assert.ok(output.args.includes("mcp__*"));
|
|
272
|
-
}
|
|
273
|
-
assert.equal(result.details.model, agent === "ask" ? "test/small" : agent === "plan" ? "test/planner" : "test/worker");
|
|
274
|
-
assert.equal(result.details.effort, agent === "ask" ? "low" : agent === "plan" ? "max" : "high");
|
|
275
|
-
assert.equal(result.usage.input, 10);
|
|
276
|
-
assert.match(result.content[0].text, new RegExp(result.details.reason));
|
|
277
|
-
assert.ok(updates.length > 0);
|
|
278
|
-
}
|
|
279
|
-
}));
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test("configured exploration always uses the small model and stays read-only", async () => {
|
|
283
|
-
await fixture(async (f) => environment(f, async () => {
|
|
284
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({
|
|
285
|
-
small_model: "test/small", delegate_exploration: true,
|
|
286
|
-
subagent_variant_overrides: { ask: { model: "test/ask", effort: "max" } },
|
|
287
|
-
}));
|
|
288
|
-
const h = harness(f);
|
|
289
|
-
await h.start();
|
|
290
|
-
assert.match(h.handlers.get("before_agent_start")({ systemPrompt: "base" }).systemPrompt, /purpose: explore/);
|
|
291
|
-
const result = await h.execute({ agent: "ask", purpose: "explore", task: "Locate configuration loading" });
|
|
292
|
-
assert.equal(result.details.model, "test/small");
|
|
293
|
-
assert.equal(result.details.effort, "low");
|
|
294
|
-
assert.equal(result.details.reason, "configured exploration small model");
|
|
295
|
-
assert.deepEqual(h.ctx.model, { provider: "test", id: "current" });
|
|
296
|
-
const output = JSON.parse(result.details.output);
|
|
297
|
-
const tools = output.args[output.args.indexOf("--tools") + 1].split(",");
|
|
298
|
-
assert.ok(!tools.includes("subagent") && !tools.includes("bash"));
|
|
299
|
-
await assert.rejects(h.execute({ agent: "plan", purpose: "explore", task: "Locate configuration loading" }), /requires agent: ask/);
|
|
300
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({ small_model: "test/small", delegate_exploration: false }));
|
|
301
|
-
await assert.rejects(h.execute({ agent: "ask", purpose: "explore", task: "Locate configuration loading" }), /Enable delegate_exploration/);
|
|
302
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({ delegate_exploration: true }));
|
|
303
|
-
await assert.rejects(h.execute({ agent: "ask", purpose: "explore", task: "Locate configuration loading" }), /requires small_model/);
|
|
304
|
-
}));
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("profile commands cancel cleanly, inject instructions, persist and restore branch state", async () => {
|
|
308
|
-
await fixture(async (f) => environment(f, async () => {
|
|
309
|
-
const h = harness(f);
|
|
310
|
-
await h.start();
|
|
311
|
-
const command = h.commands.get("agent");
|
|
312
|
-
const prompt = () => h.handlers.get("before_agent_start")({ systemPrompt: "base" }, h.ctx).systemPrompt;
|
|
313
|
-
await command.handler("", h.ctx);
|
|
314
|
-
assert.equal(h.states.length, 0);
|
|
315
|
-
await command.handler("plan", h.ctx);
|
|
316
|
-
assert.match(prompt(), /base[\s\S]*profile: plan/);
|
|
317
|
-
await assert.rejects(h.execute({ agent: "code", task: "Make changes" }), /only to plan or ask/);
|
|
318
|
-
await command.handler("nonsense", h.ctx);
|
|
319
|
-
assert.match(prompt(), /profile: plan/);
|
|
320
|
-
await command.handler("debug", h.ctx);
|
|
321
|
-
assert.match(prompt(), /trace its cause/);
|
|
322
|
-
h.states.pop();
|
|
323
|
-
h.handlers.get("session_tree")({}, h.ctx);
|
|
324
|
-
assert.match(prompt(), /profile: plan/);
|
|
325
|
-
h.states.length = 0;
|
|
326
|
-
h.handlers.get("session_tree")({}, h.ctx);
|
|
327
|
-
assert.match(prompt(), /profile: code/);
|
|
328
|
-
}));
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("Alt+A cycles all profiles, persists the selection and respects busy/non-TUI sessions", async () => {
|
|
332
|
-
await fixture(async (f) => environment(f, async () => {
|
|
333
|
-
const h = harness(f);
|
|
334
|
-
h.ctx.mode = "tui";
|
|
335
|
-
await h.start();
|
|
336
|
-
assert.equal(h.shortcuts.size, 1);
|
|
337
|
-
const shortcut = h.shortcuts.get("alt+a");
|
|
338
|
-
assert.ok(shortcut);
|
|
339
|
-
assert.ok(matchesKey("\u001ba", "alt+a"));
|
|
340
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:code");
|
|
341
|
-
for (const name of agentNames) {
|
|
342
|
-
await shortcut.handler(h.ctx);
|
|
343
|
-
assert.equal(h.statuses.get("piexis-agent"), `agent:${name}`);
|
|
344
|
-
assert.equal(h.states.at(-1).data.agent, name);
|
|
345
|
-
assert.match(h.handlers.get("before_agent_start")({ systemPrompt: "base" }).systemPrompt, new RegExp(`profile: ${name}`));
|
|
346
|
-
}
|
|
347
|
-
const count = h.states.length;
|
|
348
|
-
h.ctx.isIdle = () => false;
|
|
349
|
-
await shortcut.handler(h.ctx);
|
|
350
|
-
assert.equal(h.states.length, count);
|
|
351
|
-
assert.equal(h.notifications.at(-1)[1], "warning");
|
|
352
|
-
h.ctx.isIdle = () => true;
|
|
353
|
-
for (const mode of ["json", "rpc", "print"]) {
|
|
354
|
-
h.ctx.mode = mode;
|
|
355
|
-
await shortcut.handler(h.ctx);
|
|
356
|
-
assert.equal(h.states.length, count);
|
|
357
|
-
}
|
|
358
|
-
h.ctx.mode = "tui";
|
|
359
|
-
await h.commands.get("agent").handler("debug", h.ctx);
|
|
360
|
-
await shortcut.handler(h.ctx);
|
|
361
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:ask");
|
|
362
|
-
h.states.pop();
|
|
363
|
-
h.handlers.get("session_tree")({}, h.ctx);
|
|
364
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:debug");
|
|
365
|
-
const resumed = harness(f);
|
|
366
|
-
resumed.states.push(...h.states);
|
|
367
|
-
await resumed.start();
|
|
368
|
-
assert.equal(resumed.statuses.get("piexis-agent"), "agent:debug");
|
|
369
|
-
}));
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test("commands, picker and cycling apply merged profile models and effort", async () => {
|
|
373
|
-
await fixture(async (f) => environment(f, async () => {
|
|
374
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({
|
|
375
|
-
model: "test/main", small_model: "test/small", subagent_model: "test/worker",
|
|
376
|
-
subagent_variant_overrides: { plan: { model: "test/planner", effort: "high" }, debug: { effort: "max" } },
|
|
377
|
-
}));
|
|
378
|
-
await writeFile(join(f.cwd, ".pi", "agent.json"), JSON.stringify({
|
|
379
|
-
subagent_variant_overrides: { plan: { effort: "low" }, ask: { model: "test/answer", effort: "off" } },
|
|
380
|
-
}));
|
|
381
|
-
const h = harness(f);
|
|
382
|
-
h.ctx.mode = "tui";
|
|
383
|
-
await h.start();
|
|
384
|
-
await h.shortcuts.get("alt+a").handler(h.ctx);
|
|
385
|
-
assert.equal(h.mainModel.id, "planner");
|
|
386
|
-
assert.equal(h.effort, "low");
|
|
387
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:plan");
|
|
388
|
-
await h.commands.get("agent").handler("debug", h.ctx);
|
|
389
|
-
assert.equal(h.mainModel.id, "main");
|
|
390
|
-
assert.equal(h.effort, "max");
|
|
391
|
-
h.ctx.ui.select = async () => "ask";
|
|
392
|
-
await h.commands.get("agent").handler("", h.ctx);
|
|
393
|
-
assert.equal(h.mainModel.id, "answer");
|
|
394
|
-
assert.equal(h.effort, "off");
|
|
395
|
-
h.ctx.isProjectTrusted = () => false;
|
|
396
|
-
await h.commands.get("agent").handler("plan", h.ctx);
|
|
397
|
-
assert.equal(h.mainModel.id, "planner");
|
|
398
|
-
assert.equal(h.effort, "high");
|
|
399
|
-
}));
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("unconfigured profile fields leave current settings alone", async () => {
|
|
403
|
-
await fixture(async (f) => environment(f, async () => {
|
|
404
|
-
const h = harness(f);
|
|
405
|
-
await h.commands.get("agent").handler("plan", h.ctx);
|
|
406
|
-
assert.equal(h.mainModel, undefined);
|
|
407
|
-
assert.equal(h.effort, undefined);
|
|
408
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({
|
|
409
|
-
small_model: "test/small", subagent_model: "test/worker",
|
|
410
|
-
subagent_variant_overrides: { ask: { effort: "low" }, code: { model: "test/coder" } },
|
|
411
|
-
}));
|
|
412
|
-
await h.commands.get("agent").handler("ask", h.ctx);
|
|
413
|
-
assert.equal(h.mainModel, undefined);
|
|
414
|
-
assert.equal(h.effort, "low");
|
|
415
|
-
await h.commands.get("agent").handler("code", h.ctx);
|
|
416
|
-
assert.equal(h.mainModel.id, "coder");
|
|
417
|
-
assert.equal(h.effort, "low");
|
|
418
|
-
}));
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
test("failed profile switches preserve model, effort and profile and release the switch guard", async () => {
|
|
422
|
-
await fixture(async (f) => environment(f, async () => {
|
|
423
|
-
const h = harness(f);
|
|
424
|
-
h.ctx.mode = "tui";
|
|
425
|
-
await h.start();
|
|
426
|
-
await writeFile(join(f.agentDir, "agent.json"), JSON.stringify({
|
|
427
|
-
subagent_variant_overrides: { code: { model: "test/coder", effort: "medium" } },
|
|
428
|
-
}));
|
|
429
|
-
await h.commands.get("agent").handler("code", h.ctx);
|
|
430
|
-
const count = h.states.length;
|
|
431
|
-
for (const value of ["{bad", ...["missing", "unauthenticated"].map((id) => JSON.stringify({
|
|
432
|
-
subagent_variant_overrides: { plan: { model: `test/${id}`, effort: "max" } },
|
|
433
|
-
}))]) {
|
|
434
|
-
await writeFile(join(f.agentDir, "agent.json"), value);
|
|
435
|
-
await h.shortcuts.get("alt+a").handler(h.ctx);
|
|
436
|
-
assert.equal(h.mainModel.id, "coder");
|
|
437
|
-
assert.equal(h.effort, "medium");
|
|
438
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:code");
|
|
439
|
-
assert.equal(h.states.length, count);
|
|
440
|
-
assert.equal(h.notifications.at(-1)[1], "warning");
|
|
441
|
-
}
|
|
442
|
-
await writeFile(join(f.agentDir, "agent.json"), "{}");
|
|
443
|
-
await Promise.all([h.shortcuts.get("alt+a").handler(h.ctx), h.commands.get("agent").handler("ask", h.ctx)]);
|
|
444
|
-
assert.equal(h.states.length, count + 1);
|
|
445
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:plan");
|
|
446
|
-
await h.commands.get("agent").handler("ask", h.ctx);
|
|
447
|
-
assert.equal(h.statuses.get("piexis-agent"), "agent:ask");
|
|
448
|
-
}));
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
test("footer reads live agent status after commands, cycling and tree restoration without losing width bounds", async () => {
|
|
452
|
-
await fixture(async (f) => environment(f, async () => {
|
|
453
|
-
const h = harness(f);
|
|
454
|
-
h.ctx.mode = "tui";
|
|
455
|
-
h.ctx.getContextUsage = () => undefined;
|
|
456
|
-
const uiHandlers = new Map();
|
|
457
|
-
const theme = { fg: (_color, text) => `\u001b[36m${text}\u001b[39m`, bold: (text) => `\u001b[1m${text}\u001b[22m` };
|
|
458
|
-
let footer;
|
|
459
|
-
Object.assign(h.ctx.ui, {
|
|
460
|
-
setTitle() {}, setHeader() {},
|
|
461
|
-
setFooter(make) { footer = make({ requestRender() {} }, theme, {
|
|
462
|
-
getGitBranch: () => "main", getExtensionStatuses: () => h.statuses, onBranchChange: () => () => {},
|
|
463
|
-
}); },
|
|
464
|
-
});
|
|
465
|
-
uiExtension({ on(name, handler) { uiHandlers.set(name, handler); }, getThinkingLevel: () => "high" });
|
|
466
|
-
await uiHandlers.get("session_start")({}, h.ctx);
|
|
467
|
-
try {
|
|
468
|
-
assert.doesNotMatch(footer.render(160).join("\n"), /agent:/);
|
|
469
|
-
await h.start();
|
|
470
|
-
assert.match(footer.render(160).join("\n"), /agent:code/);
|
|
471
|
-
assert.match(footer.render(160).join("\n"), /Alt\+A/);
|
|
472
|
-
for (const name of agentNames) {
|
|
473
|
-
await h.shortcuts.get("alt+a").handler(h.ctx);
|
|
474
|
-
for (const width of [40, 60, 80, 160]) {
|
|
475
|
-
const rows = footer.render(width);
|
|
476
|
-
assert.match(rows.join("\n"), new RegExp(`agent:${name}`));
|
|
477
|
-
for (const row of rows) assert.ok(visibleWidth(row) <= width);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
await h.commands.get("agent").handler("debug", h.ctx);
|
|
481
|
-
assert.match(footer.render(160).join("\n"), /agent:debug/);
|
|
482
|
-
h.states.pop();
|
|
483
|
-
h.handlers.get("session_tree")({}, h.ctx);
|
|
484
|
-
assert.match(footer.render(160).join("\n"), /agent:code/);
|
|
485
|
-
h.ctx.model = { id: "研究🚀".repeat(100) };
|
|
486
|
-
for (let width = 0; width <= 160; width++) {
|
|
487
|
-
for (const row of footer.render(width)) assert.ok(visibleWidth(row) <= width);
|
|
488
|
-
}
|
|
489
|
-
assert.match(footer.render(40).join("\n"), /agent:code/);
|
|
490
|
-
h.statuses.clear();
|
|
491
|
-
assert.doesNotMatch(footer.render(160).join("\n"), /agent:|Alt\+A/);
|
|
492
|
-
} finally { footer.dispose(); }
|
|
493
|
-
}));
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test("configured main model initializes only new sessions, not CLI overrides or resumes", async () => {
|
|
497
|
-
await fixture(async (f) => environment(f, async () => {
|
|
498
|
-
await writeFile(join(f.agentDir, "agents.json"), '{"model":"test/main"}');
|
|
499
|
-
const h = harness(f, "ask");
|
|
500
|
-
await h.start();
|
|
501
|
-
assert.equal(h.mainModel.id, "main");
|
|
502
|
-
assert.match(h.handlers.get("before_agent_start")({ systemPrompt: "base" }).systemPrompt, /profile: ask/);
|
|
503
|
-
process.argv.push("--model", "test/explicit");
|
|
504
|
-
const explicit = harness(f);
|
|
505
|
-
await explicit.start();
|
|
506
|
-
assert.equal(explicit.mainModel, undefined);
|
|
507
|
-
process.argv.length -= 2;
|
|
508
|
-
const resumed = harness(f);
|
|
509
|
-
resumed.states.push({ type: "message" });
|
|
510
|
-
await resumed.start();
|
|
511
|
-
assert.equal(resumed.mainModel, undefined);
|
|
512
|
-
}));
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("invalid requests, unavailable models and depth limits fail without spawning", async () => {
|
|
516
|
-
await fixture(async (f) => environment(f, async () => {
|
|
517
|
-
const h = harness(f);
|
|
518
|
-
for (const params of [{ agent: "worker", task: "task" }, { agent: "code", task: " " }]) await assert.rejects(h.execute(params));
|
|
519
|
-
for (const id of ["missing", "unauthenticated"]) {
|
|
520
|
-
await writeFile(join(f.agentDir, "agents.json"), JSON.stringify({ subagent_model: `test/${id}` }));
|
|
521
|
-
await assert.rejects(h.execute({ agent: "code", task: "task" }), /unavailable/);
|
|
522
|
-
}
|
|
523
|
-
process.env.PIEXIS_SUBAGENT_DEPTH = "2";
|
|
524
|
-
const nested = harness(f);
|
|
525
|
-
await nested.start();
|
|
526
|
-
assert.ok(!nested.active.includes("subagent"));
|
|
527
|
-
await assert.rejects(nested.execute({ agent: "orchestrator", task: "task" }), /depth limit/);
|
|
528
|
-
}));
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test("runner handles UTF-8 framing, multiple text blocks, tool usage, retries and failed responses", async () => {
|
|
532
|
-
await fixture(async (f) => {
|
|
533
|
-
const usage = emptyUsage(); usage.input = 4; usage.totalTokens = 4;
|
|
534
|
-
const events = [
|
|
535
|
-
{ type: "message_end", message: { role: "assistant", content: [], stopReason: "error", errorMessage: "temporary", usage } },
|
|
536
|
-
{ type: "message_end", message: { role: "toolResult", usage } },
|
|
537
|
-
{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "你好\u2028🌍" }, { type: "text", text: "done" }], stopReason: "stop", usage } },
|
|
538
|
-
];
|
|
539
|
-
await writeFile(f.cli, `const data = Buffer.from(${JSON.stringify(events.map((event) => JSON.stringify(event)).join("\n"))}); for (const byte of data) process.stdout.write(Buffer.from([byte]));`);
|
|
540
|
-
const run = () => runSubagent({ invocation: { command: process.execPath, args: [f.cli] }, args: [], cwd: f.cwd, depth: 1, task: "task", signal: new AbortController().signal });
|
|
541
|
-
const result = await run();
|
|
542
|
-
assert.equal(result.output, "你好\u2028🌍\ndone");
|
|
543
|
-
assert.equal(result.error, undefined);
|
|
544
|
-
assert.equal(result.usage.input, 12);
|
|
545
|
-
await writeFile(f.cli, `console.log(${JSON.stringify(JSON.stringify({ type: "message_end", message: { role: "assistant", content: [], stopReason: "error", errorMessage: "provider failed" } }))});`);
|
|
546
|
-
const failed = await run();
|
|
547
|
-
assert.equal(failed.exitCode, 0);
|
|
548
|
-
assert.equal(failed.error, "provider failed");
|
|
549
|
-
await writeFile(f.cli, 'process.stderr.write("startup failed"); process.exitCode = 3;');
|
|
550
|
-
assert.equal((await run()).exitCode, 3);
|
|
551
|
-
await writeFile(f.cli, 'console.log("not json");');
|
|
552
|
-
await assert.rejects(run(), /Invalid JSON/);
|
|
553
|
-
assert.match(limitOutput("x".repeat(60_000)), /truncated/);
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
test("real Pi CLI delegates to automatic and explicit small models without network or credentials", { timeout: 30_000 }, async () => {
|
|
558
|
-
await fixture(async (f) => {
|
|
559
|
-
for (const mode of ["lookup", "orchestrator", "exploration"]) {
|
|
560
|
-
const orchestrate = mode === "orchestrator";
|
|
561
|
-
const exploration = mode === "exploration";
|
|
562
|
-
await writeFile(join(f.agentDir, "agents.json"), JSON.stringify({ small_model: "piexis-test/small", delegate_exploration: true }));
|
|
563
|
-
const invocation = piInvocation();
|
|
564
|
-
const args = [...invocation.args, "--offline", "--no-session", "--mode", "json", "--no-extensions", "--no-skills", "--no-context-files", "--no-prompt-templates",
|
|
565
|
-
"-e", fileURLToPath(new URL("../extensions/agents.ts", import.meta.url)),
|
|
566
|
-
"-e", fileURLToPath(new URL("./fixtures/agents-provider.ts", import.meta.url)),
|
|
567
|
-
"--model", "piexis-test/main", "--tools", "subagent,read", "--", orchestrate ? "Use orchestrator for a lookup" : exploration ? "Delegate exploration" : "Delegate a lookup"];
|
|
568
|
-
const executionPromise = promisify(execFile)(invocation.command, args, {
|
|
569
|
-
cwd: f.cwd, env: { ...process.env, PI_CODING_AGENT_DIR: f.agentDir, PIEXIS_SUBAGENT_DEPTH: "0", PI_OFFLINE: "1" },
|
|
570
|
-
timeout: 25_000, maxBuffer: 4 * 1024 * 1024,
|
|
571
|
-
});
|
|
572
|
-
executionPromise.child.stdin.end();
|
|
573
|
-
const { stdout } = await executionPromise;
|
|
574
|
-
const events = stdout.trim().split("\n").map((line) => JSON.parse(line));
|
|
575
|
-
const execution = events.find((event) => event.type === "tool_execution_end" && event.toolName === "subagent");
|
|
576
|
-
assert.ok(execution, stdout);
|
|
577
|
-
assert.equal(execution.isError, false, JSON.stringify(execution));
|
|
578
|
-
assert.equal(execution.result.details.model, orchestrate ? "piexis-test/main" : "piexis-test/small");
|
|
579
|
-
assert.equal(execution.result.details.reason, orchestrate ? "default subagent" : exploration ? "configured exploration small model" : "automatic small task");
|
|
580
|
-
if (orchestrate) assert.match(execution.result.details.output, /child:small;depth:2/);
|
|
581
|
-
else assert.equal(execution.result.details.output, "child:small;depth:1");
|
|
582
|
-
assert.equal(execution.result.usage.input, orchestrate ? 30 : 10);
|
|
583
|
-
assert.ok(events.some((event) => event.type === "message_end" && event.message.content?.some((part) => part.text === "parent completed")));
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
test("abort and shutdown terminate running children; pre-aborted calls never start", { timeout: 10_000 }, async () => {
|
|
589
|
-
await fixture(async (f) => environment(f, async () => {
|
|
590
|
-
await writeFile(f.cli, 'process.on("SIGTERM", () => {}); console.log(JSON.stringify({type:"tool_execution_start",toolName:"read"})); setInterval(() => {}, 1000);');
|
|
591
|
-
const h = harness(f);
|
|
592
|
-
const pre = new AbortController(); pre.abort();
|
|
593
|
-
await assert.rejects(h.execute({ agent: "code", task: "task" }, pre.signal), { name: "AbortError" });
|
|
594
|
-
for (const shutdown of [false, true]) {
|
|
595
|
-
const controller = new AbortController();
|
|
596
|
-
let ready;
|
|
597
|
-
const started = new Promise((resolve) => { ready = resolve; });
|
|
598
|
-
const pending = h.execute({ agent: "code", task: "task" }, controller.signal, ready);
|
|
599
|
-
const rejected = assert.rejects(pending, /aborted/);
|
|
600
|
-
await started;
|
|
601
|
-
if (shutdown) await h.handlers.get("session_shutdown")(); else controller.abort();
|
|
602
|
-
await rejected;
|
|
603
|
-
}
|
|
604
|
-
}));
|
|
605
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import test from "node:test";
|
|
5
|
-
|
|
6
|
-
const python = spawnSync("python3", ["-c", "import pty, termios"], { encoding: "utf8" });
|
|
7
|
-
const driver = fileURLToPath(new URL("fixtures/ask-question-pty.py", import.meta.url));
|
|
8
|
-
const fixture = fileURLToPath(new URL("fixtures/ask-question-terminal.mjs", import.meta.url));
|
|
9
|
-
|
|
10
|
-
for (const scenario of ["single", "multi", "text", "other", "navigation", "cancel", "ctrl-c", "abort"]) {
|
|
11
|
-
test(`real terminal: ${scenario}, focus restoration and cleanup`, { skip: python.status !== 0 ? "Python/Unix PTY support unavailable" : false }, () => {
|
|
12
|
-
const result = spawnSync("python3", [driver, fixture, scenario, process.execPath], { encoding: "utf8", timeout: 12000 });
|
|
13
|
-
assert.equal(result.status, 0, result.stderr + result.stdout);
|
|
14
|
-
assert.equal(JSON.parse(result.stdout).terminalRestored, true);
|
|
15
|
-
});
|
|
16
|
-
}
|