@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.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/CHANGELOG.md +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import type { SSHConnectionTarget } from "../connection-manager";
|
|
3
|
-
import * as connectionManager from "../connection-manager";
|
|
4
|
-
import { listRemoteDir, readRemoteFile, statRemotePath, writeRemoteFile } from "../file-transfer";
|
|
5
|
-
|
|
6
|
-
describe("ssh file-transfer POSIX guard", () => {
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
vi.restoreAllMocks();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("rejects a confirmed Windows remote before running any POSIX command", async () => {
|
|
12
|
-
// Stub BOTH the connection and the host-info probe so the guard is reached
|
|
13
|
-
// without opening a real SSH connection and before any command is spawned.
|
|
14
|
-
const ensureConnectionSpy = vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
|
|
15
|
-
const ensureHostInfoSpy = vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
|
|
16
|
-
version: 4,
|
|
17
|
-
os: "windows",
|
|
18
|
-
shell: "powershell",
|
|
19
|
-
compatEnabled: false,
|
|
20
|
-
});
|
|
21
|
-
const target: SSHConnectionTarget = { name: "winbox", host: "winbox" };
|
|
22
|
-
await expect(readRemoteFile(target, "C:/x.txt", { maxBytes: 1024 })).rejects.toThrow(/Windows host/);
|
|
23
|
-
await expect(writeRemoteFile(target, "C:/x.txt", new Uint8Array([1]), {})).rejects.toThrow(/Windows host/);
|
|
24
|
-
// Prove the guard ran through the stubbed transport rather than failing early
|
|
25
|
-
// for an unrelated reason (e.g. a future import refactor bypassing the mocks).
|
|
26
|
-
expect(ensureConnectionSpy).toHaveBeenCalled();
|
|
27
|
-
expect(ensureHostInfoSpy).toHaveBeenCalled();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("rejects a non-Windows remote with no verified transferShell", async () => {
|
|
31
|
-
// No transferShell means the capability probe never confirmed any of
|
|
32
|
-
// sh/bash/zsh works. The guard refuses regardless of `shell` because the
|
|
33
|
-
// real ssh:// contract is "did we verify a POSIX shell works", not
|
|
34
|
-
// "what name did the login shell self-report" (#3719).
|
|
35
|
-
vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
|
|
36
|
-
vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
|
|
37
|
-
version: 4,
|
|
38
|
-
os: "linux",
|
|
39
|
-
shell: "unknown",
|
|
40
|
-
compatEnabled: false,
|
|
41
|
-
});
|
|
42
|
-
const target: SSHConnectionTarget = { name: "noshell", host: "noshell" };
|
|
43
|
-
await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/no verified POSIX shell/);
|
|
44
|
-
await expect(writeRemoteFile(target, "/tmp/x", new Uint8Array([1]), {})).rejects.toThrow(
|
|
45
|
-
/no verified POSIX shell/,
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("dispatches transfer commands through the verified transferShell, not the login shell", async () => {
|
|
50
|
-
// The bug fix: if the login shell is fish/csh/tcsh, the legacy guard
|
|
51
|
-
// would refuse the host — but allowing it isn't enough on its own.
|
|
52
|
-
// OpenSSH still hands our snippets to `$SHELL -c`, so a fish login
|
|
53
|
-
// shell would choke on `if [ … ]; then …`. Every transfer command
|
|
54
|
-
// must be wrapped in `<transferShell> -c '…'` to force parsing
|
|
55
|
-
// under the shell we verified can run it (#3719).
|
|
56
|
-
vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
|
|
57
|
-
vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
|
|
58
|
-
version: 4,
|
|
59
|
-
os: "linux",
|
|
60
|
-
// Login shell is fish; only `transferShell` indicates a working POSIX shell.
|
|
61
|
-
shell: "unknown",
|
|
62
|
-
transferShell: "bash",
|
|
63
|
-
compatEnabled: false,
|
|
64
|
-
});
|
|
65
|
-
const buildSpy = vi
|
|
66
|
-
.spyOn(connectionManager, "buildRemoteCommand")
|
|
67
|
-
.mockRejectedValue(new Error("stop-before-spawn"));
|
|
68
|
-
const target: SSHConnectionTarget = { name: "fishbox", host: "fishbox" };
|
|
69
|
-
|
|
70
|
-
await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/stop-before-spawn/);
|
|
71
|
-
await expect(writeRemoteFile(target, "/tmp/x", new Uint8Array([1]), {})).rejects.toThrow(/stop-before-spawn/);
|
|
72
|
-
await expect(statRemotePath(target, "/etc/hosts")).rejects.toThrow(/stop-before-spawn/);
|
|
73
|
-
await expect(listRemoteDir(target, "/etc")).rejects.toThrow(/stop-before-spawn/);
|
|
74
|
-
|
|
75
|
-
// Each dispatch must start with `bash -c '…'` and embed the original
|
|
76
|
-
// POSIX snippet inside the quoted command. Read also drops `-n`
|
|
77
|
-
// (allowStdin: true) because cat-staging needs stdin streaming.
|
|
78
|
-
const dispatches = buildSpy.mock.calls.map(call => call[1] as string);
|
|
79
|
-
expect(dispatches[0]).toMatch(/^bash -c '.*head -c 1025/);
|
|
80
|
-
expect(dispatches[1]).toMatch(/^bash -c '.*cat > /);
|
|
81
|
-
expect(buildSpy.mock.calls[1]?.[2]).toMatchObject({ allowStdin: true });
|
|
82
|
-
expect(dispatches[2]).toMatch(/^bash -c '.*if \[ -d /);
|
|
83
|
-
expect(dispatches[3]).toMatch(/^bash -c '.*LC_ALL=C ls -1Ap /);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("uses sh -c when transferShell is sh (the most universal POSIX fallback)", async () => {
|
|
87
|
-
// Belt-and-suspenders: the common happy path with a sh-family login
|
|
88
|
-
// shell still routes through `sh -c` to keep one dispatch shape.
|
|
89
|
-
vi.spyOn(connectionManager, "ensureConnection").mockResolvedValue(undefined);
|
|
90
|
-
vi.spyOn(connectionManager, "ensureHostInfo").mockResolvedValue({
|
|
91
|
-
version: 4,
|
|
92
|
-
os: "linux",
|
|
93
|
-
shell: "sh",
|
|
94
|
-
transferShell: "sh",
|
|
95
|
-
compatEnabled: false,
|
|
96
|
-
});
|
|
97
|
-
const buildSpy = vi
|
|
98
|
-
.spyOn(connectionManager, "buildRemoteCommand")
|
|
99
|
-
.mockRejectedValue(new Error("stop-before-spawn"));
|
|
100
|
-
const target: SSHConnectionTarget = { name: "shbox", host: "shbox" };
|
|
101
|
-
|
|
102
|
-
await expect(readRemoteFile(target, "/etc/hosts", { maxBytes: 1024 })).rejects.toThrow(/stop-before-spawn/);
|
|
103
|
-
expect(buildSpy.mock.calls[0]?.[1]).toMatch(/^sh -c '.*head -c 1025/);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { isMounted } from "../sshfs-mount";
|
|
4
|
-
|
|
5
|
-
describe("isMounted", () => {
|
|
6
|
-
it("detects a macOS mount point when mountpoint is unavailable", async () => {
|
|
7
|
-
const parentPath = import.meta.dir;
|
|
8
|
-
const mountPath = path.join(parentPath, "mounted");
|
|
9
|
-
const stat = async (filePath: string) => ({ dev: filePath === mountPath ? 2 : 1 });
|
|
10
|
-
|
|
11
|
-
await expect(isMounted(mountPath, { platform: "darwin", stat, which: () => null })).resolves.toBe(true);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, spyOn } from "bun:test";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { buildSystemPrompt } from "./system-prompt";
|
|
6
|
-
|
|
7
|
-
interface ProbeRunResult {
|
|
8
|
-
elapsedMs: number;
|
|
9
|
-
childElapsedMs: number;
|
|
10
|
-
cached: unknown;
|
|
11
|
-
count: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function runProbeScenario(options: {
|
|
15
|
-
runs: number;
|
|
16
|
-
sleepSeconds?: number;
|
|
17
|
-
holdStdoutOpen?: boolean;
|
|
18
|
-
descendantHoldsStdout?: boolean;
|
|
19
|
-
validOutput?: string;
|
|
20
|
-
}): Promise<ProbeRunResult> {
|
|
21
|
-
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gpu-probe-"));
|
|
22
|
-
try {
|
|
23
|
-
const binDir = path.join(tempRoot, "bin");
|
|
24
|
-
const cacheRoot = path.join(tempRoot, "cache");
|
|
25
|
-
const probeCountPath = path.join(tempRoot, "probe-count");
|
|
26
|
-
await fs.mkdir(binDir, { recursive: true });
|
|
27
|
-
await fs.mkdir(path.join(cacheRoot, "omp"), { recursive: true });
|
|
28
|
-
const lspciPath = path.join(binDir, "lspci");
|
|
29
|
-
await Bun.write(
|
|
30
|
-
lspciPath,
|
|
31
|
-
'#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ -n "$OMP_GPU_PROBE_VALID_OUTPUT" ]; then printf "%s\\n" "$OMP_GPU_PROBE_VALID_OUTPUT"; fi\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n',
|
|
32
|
-
);
|
|
33
|
-
await fs.chmod(lspciPath, 0o755);
|
|
34
|
-
|
|
35
|
-
const scenarioPath = path.join(tempRoot, "scenario.ts");
|
|
36
|
-
await Bun.write(
|
|
37
|
-
scenarioPath,
|
|
38
|
-
`import { getGpuCachePath, refreshDirsFromEnv } from ${JSON.stringify(path.resolve(import.meta.dir, "../../utils/src/index.ts"))};
|
|
39
|
-
import { buildSystemPrompt } from ${JSON.stringify(path.join(import.meta.dir, "system-prompt.ts"))};
|
|
40
|
-
|
|
41
|
-
refreshDirsFromEnv();
|
|
42
|
-
const buildOptions = {
|
|
43
|
-
contextFiles: [],
|
|
44
|
-
skills: [],
|
|
45
|
-
toolNames: [],
|
|
46
|
-
workspaceTree: {
|
|
47
|
-
rootPath: process.cwd(),
|
|
48
|
-
rendered: "",
|
|
49
|
-
truncated: false,
|
|
50
|
-
totalLines: 0,
|
|
51
|
-
agentsMdFiles: [],
|
|
52
|
-
},
|
|
53
|
-
activeRepoContext: null,
|
|
54
|
-
};
|
|
55
|
-
const startedAt = performance.now();
|
|
56
|
-
for (let index = 0; index < Number(process.env.OMP_GPU_PROBE_RUNS ?? "1"); index += 1) {
|
|
57
|
-
await buildSystemPrompt(buildOptions);
|
|
58
|
-
}
|
|
59
|
-
const cacheFile = Bun.file(getGpuCachePath());
|
|
60
|
-
const cached = await cacheFile.exists() ? await cacheFile.json() : null;
|
|
61
|
-
const countFile = Bun.file(process.env.OMP_GPU_PROBE_COUNT ?? "");
|
|
62
|
-
const count = await countFile.exists() ? (await countFile.text()).length : 0;
|
|
63
|
-
console.log(JSON.stringify({ elapsedMs: Math.round(performance.now() - startedAt), cached, count }));
|
|
64
|
-
`,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const env: Record<string, string | undefined> = {
|
|
68
|
-
...process.env,
|
|
69
|
-
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
70
|
-
XDG_CACHE_HOME: cacheRoot,
|
|
71
|
-
OMP_GPU_PROBE_COUNT: probeCountPath,
|
|
72
|
-
OMP_GPU_PROBE_RUNS: String(options.runs),
|
|
73
|
-
};
|
|
74
|
-
// Strip inherited dirs-resolver overrides so XDG_CACHE_HOME above wins and
|
|
75
|
-
// the test cannot touch the developer/CI profile's real gpu_cache.json.
|
|
76
|
-
for (const key of ["PI_CODING_AGENT_DIR", "OMP_PROFILE", "PI_PROFILE", "PI_CONFIG_DIR"]) {
|
|
77
|
-
delete env[key];
|
|
78
|
-
}
|
|
79
|
-
if (options.sleepSeconds === undefined) {
|
|
80
|
-
delete env.OMP_GPU_PROBE_SLEEP;
|
|
81
|
-
} else {
|
|
82
|
-
env.OMP_GPU_PROBE_SLEEP = String(options.sleepSeconds);
|
|
83
|
-
}
|
|
84
|
-
if (options.holdStdoutOpen) {
|
|
85
|
-
env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN = "true";
|
|
86
|
-
} else {
|
|
87
|
-
delete env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN;
|
|
88
|
-
}
|
|
89
|
-
if (options.descendantHoldsStdout) {
|
|
90
|
-
env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT = "true";
|
|
91
|
-
} else {
|
|
92
|
-
delete env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT;
|
|
93
|
-
}
|
|
94
|
-
if (options.validOutput !== undefined) {
|
|
95
|
-
env.OMP_GPU_PROBE_VALID_OUTPUT = options.validOutput;
|
|
96
|
-
} else {
|
|
97
|
-
delete env.OMP_GPU_PROBE_VALID_OUTPUT;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const childStartedAt = performance.now();
|
|
101
|
-
const child = Bun.spawn([process.execPath, scenarioPath], { stdout: "pipe", stderr: "pipe", env });
|
|
102
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
103
|
-
new Response(child.stdout).text(),
|
|
104
|
-
new Response(child.stderr).text(),
|
|
105
|
-
child.exited,
|
|
106
|
-
]);
|
|
107
|
-
const childElapsedMs = Math.round(performance.now() - childStartedAt);
|
|
108
|
-
if (exitCode !== 0) {
|
|
109
|
-
throw new Error(`GPU probe scenario failed with exit ${exitCode}: ${stderr}`);
|
|
110
|
-
}
|
|
111
|
-
return { ...JSON.parse(stdout.trim()), childElapsedMs };
|
|
112
|
-
} finally {
|
|
113
|
-
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => {
|
|
118
|
-
it("caches empty GPU probe results", async () => {
|
|
119
|
-
const result = await runProbeScenario({ runs: 2 });
|
|
120
|
-
|
|
121
|
-
expect(result.cached).toEqual({ gpu: null });
|
|
122
|
-
expect(result.count).toBe(1);
|
|
123
|
-
}, 15_000);
|
|
124
|
-
|
|
125
|
-
it("kills the GPU probe at the prep deadline", async () => {
|
|
126
|
-
const result = await runProbeScenario({ runs: 1, sleepSeconds: 12, holdStdoutOpen: true });
|
|
127
|
-
|
|
128
|
-
expect(result.cached).toEqual({ gpu: null });
|
|
129
|
-
// Probe is SIGKILLed at ~4.5s and the drain wait is bounded, so in-child
|
|
130
|
-
// time sits near the deadline; waiting on the descendant would push it
|
|
131
|
-
// past the 12s sleep.
|
|
132
|
-
expect(result.elapsedMs).toBeLessThan(6500);
|
|
133
|
-
// Codex#3838: the child process MUST exit shortly after the deadline, not
|
|
134
|
-
// linger until a descendant holding stdout (sleep 12) exits on its own.
|
|
135
|
-
// The bound over in-child time budgets bun spawn/startup on loaded runners
|
|
136
|
-
// while staying far below the descendant's 12s exit.
|
|
137
|
-
expect(result.childElapsedMs).toBeLessThan(9000);
|
|
138
|
-
}, 20_000);
|
|
139
|
-
|
|
140
|
-
it("does not wait on stdout held by a descendant after a successful probe", async () => {
|
|
141
|
-
const result = await runProbeScenario({ runs: 1, sleepSeconds: 8, descendantHoldsStdout: true });
|
|
142
|
-
|
|
143
|
-
expect(result.cached).toEqual({ gpu: null });
|
|
144
|
-
// Probe exits 0 immediately but leaves a backgrounded sleep holding the stdout
|
|
145
|
-
// pipe. The success path MUST bound the drain wait, not block until sleep exits.
|
|
146
|
-
expect(result.elapsedMs).toBeLessThan(2000);
|
|
147
|
-
// Budgets bun spawn/startup overhead; blocking on the descendant would
|
|
148
|
-
// take at least the 8s sleep.
|
|
149
|
-
expect(result.childElapsedMs).toBeLessThan(5000);
|
|
150
|
-
}, 20_000);
|
|
151
|
-
|
|
152
|
-
it("keeps probe output captured before a descendant delays EOF", async () => {
|
|
153
|
-
const result = await runProbeScenario({
|
|
154
|
-
runs: 1,
|
|
155
|
-
sleepSeconds: 8,
|
|
156
|
-
descendantHoldsStdout: true,
|
|
157
|
-
validOutput: "00:02.0 VGA compatible controller: NVIDIA TestGPU",
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Probe exited 0 with valid output before bg sleep held stdout open.
|
|
161
|
-
// Captured stdout MUST be cached, not discarded as if the probe failed.
|
|
162
|
-
expect(result.cached).toEqual({ gpu: "02.0 VGA compatible controller: NVIDIA TestGPU" });
|
|
163
|
-
expect(result.elapsedMs).toBeLessThan(2000);
|
|
164
|
-
// Budgets bun spawn/startup overhead; blocking on the descendant would
|
|
165
|
-
// take at least the 8s sleep.
|
|
166
|
-
expect(result.childElapsedMs).toBeLessThan(5000);
|
|
167
|
-
}, 20_000);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe.skipIf(process.platform !== "linux")("system prompt CPU model", () => {
|
|
171
|
-
it("does not call os.cpus while building the workstation block", async () => {
|
|
172
|
-
const cpus = spyOn(os, "cpus").mockImplementation(() => [
|
|
173
|
-
{
|
|
174
|
-
model: "Synthetic Slow CPU",
|
|
175
|
-
speed: 0,
|
|
176
|
-
times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
|
|
177
|
-
},
|
|
178
|
-
]);
|
|
179
|
-
try {
|
|
180
|
-
await buildSystemPrompt({
|
|
181
|
-
resolvedCustomPrompt: "Base prompt",
|
|
182
|
-
contextFiles: [],
|
|
183
|
-
skills: [],
|
|
184
|
-
rules: [],
|
|
185
|
-
workspaceTree: {
|
|
186
|
-
rootPath: import.meta.dir,
|
|
187
|
-
rendered: "",
|
|
188
|
-
truncated: false,
|
|
189
|
-
totalLines: 0,
|
|
190
|
-
agentsMdFiles: [],
|
|
191
|
-
},
|
|
192
|
-
activeRepoContext: null,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
expect(cpus).not.toHaveBeenCalled();
|
|
196
|
-
} finally {
|
|
197
|
-
cpus.mockRestore();
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("non-Linux system prompt CPU model", () => {
|
|
203
|
-
it("includes the model returned by os.cpus", async () => {
|
|
204
|
-
const originalPlatform = process.platform;
|
|
205
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
206
|
-
const cpus = spyOn(os, "cpus").mockImplementation(() => [
|
|
207
|
-
{
|
|
208
|
-
model: "Synthetic Non-Linux CPU",
|
|
209
|
-
speed: 0,
|
|
210
|
-
times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
|
|
211
|
-
},
|
|
212
|
-
]);
|
|
213
|
-
try {
|
|
214
|
-
const systemPrompt = await buildSystemPrompt({
|
|
215
|
-
resolvedCustomPrompt: "Base prompt",
|
|
216
|
-
contextFiles: [],
|
|
217
|
-
skills: [],
|
|
218
|
-
rules: [],
|
|
219
|
-
workspaceTree: {
|
|
220
|
-
rootPath: import.meta.dir,
|
|
221
|
-
rendered: "",
|
|
222
|
-
truncated: false,
|
|
223
|
-
totalLines: 0,
|
|
224
|
-
agentsMdFiles: [],
|
|
225
|
-
},
|
|
226
|
-
activeRepoContext: null,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
expect(cpus).toHaveBeenCalledTimes(1);
|
|
230
|
-
expect(systemPrompt.systemPrompt.join("\n")).toContain("- CPU: Synthetic Non-Linux CPU");
|
|
231
|
-
} finally {
|
|
232
|
-
cpus.mockRestore();
|
|
233
|
-
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
});
|
package/src/task/render.test.ts
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
-
import { Settings } from "../config/settings";
|
|
3
|
-
import { getThemeByName, setThemeInstance, type Theme } from "../modes/theme/theme";
|
|
4
|
-
import { renderResult } from "./render";
|
|
5
|
-
import { taskToolRenderer } from "./renderer";
|
|
6
|
-
import type { AgentProgress, SingleResult, TaskToolDetails } from "./types";
|
|
7
|
-
|
|
8
|
-
const strip = (lines: readonly string[]): string =>
|
|
9
|
-
lines
|
|
10
|
-
.join("\n")
|
|
11
|
-
.replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "")
|
|
12
|
-
.replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
-
|
|
14
|
-
const originalRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "rows");
|
|
15
|
-
|
|
16
|
-
function setViewportRows(rows: number): void {
|
|
17
|
-
Object.defineProperty(process.stdout, "rows", { configurable: true, value: rows });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function restoreViewportRows(): void {
|
|
21
|
-
if (originalRowsDescriptor) {
|
|
22
|
-
Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
Reflect.deleteProperty(process.stdout, "rows");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function makeProgress(recentOutput: string[]): AgentProgress {
|
|
29
|
-
return {
|
|
30
|
-
index: 0,
|
|
31
|
-
id: "NoisySubagent",
|
|
32
|
-
agent: "task",
|
|
33
|
-
agentSource: "bundled",
|
|
34
|
-
status: "running",
|
|
35
|
-
task: "produce noisy output",
|
|
36
|
-
recentTools: [],
|
|
37
|
-
recentOutput,
|
|
38
|
-
toolCount: 1,
|
|
39
|
-
requests: 1,
|
|
40
|
-
tokens: 0,
|
|
41
|
-
cost: 0,
|
|
42
|
-
durationMs: 0,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeParentWithNestedProgress(childCount: number): AgentProgress {
|
|
47
|
-
const children = Array.from(
|
|
48
|
-
{ length: childCount },
|
|
49
|
-
(_, index): AgentProgress => ({
|
|
50
|
-
...makeProgress([]),
|
|
51
|
-
index,
|
|
52
|
-
id: `Nested${index + 1}`,
|
|
53
|
-
task: `nested child ${index + 1}`,
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
return {
|
|
57
|
-
...makeProgress([]),
|
|
58
|
-
id: "Parent",
|
|
59
|
-
task: "parent task",
|
|
60
|
-
inflightTaskDetails: {
|
|
61
|
-
projectAgentsDir: null,
|
|
62
|
-
results: [],
|
|
63
|
-
totalDurationMs: 1,
|
|
64
|
-
progress: children,
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function makeSingleResult(index: number, overrides?: Partial<SingleResult>): SingleResult {
|
|
70
|
-
return {
|
|
71
|
-
index,
|
|
72
|
-
id: `Done${index + 1}`,
|
|
73
|
-
agent: "task",
|
|
74
|
-
agentSource: "bundled",
|
|
75
|
-
task: `finished child ${index + 1}`,
|
|
76
|
-
exitCode: 0,
|
|
77
|
-
output: "ok",
|
|
78
|
-
stderr: "",
|
|
79
|
-
truncated: false,
|
|
80
|
-
durationMs: 5,
|
|
81
|
-
tokens: 0,
|
|
82
|
-
requests: 1,
|
|
83
|
-
...overrides,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function makeParentWithNestedResults(childCount: number): TaskToolDetails {
|
|
88
|
-
const nested: TaskToolDetails = {
|
|
89
|
-
projectAgentsDir: null,
|
|
90
|
-
// Failure on the LAST child in display order (equal durations, index tiebreak):
|
|
91
|
-
// a plain head-of-list pick would elide it, so its visibility proves the
|
|
92
|
-
// failure-first selection of `selectCollapsedResults`.
|
|
93
|
-
results: Array.from({ length: childCount }, (_, index) =>
|
|
94
|
-
makeSingleResult(index, index === childCount - 1 ? { exitCode: 1, error: "boom" } : undefined),
|
|
95
|
-
),
|
|
96
|
-
totalDurationMs: 1,
|
|
97
|
-
};
|
|
98
|
-
const parent = makeSingleResult(0, { id: "Parent", extractedToolData: { task: [nested] } });
|
|
99
|
-
return { projectAgentsDir: null, results: [parent], totalDurationMs: 1 };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function renderResultText(details: TaskToolDetails, expanded: boolean, uiTheme: Theme): string {
|
|
103
|
-
const component = renderResult(
|
|
104
|
-
{ content: [{ type: "text", text: "Ran 1 agent" }], details },
|
|
105
|
-
{ expanded, isPartial: false },
|
|
106
|
-
uiTheme,
|
|
107
|
-
);
|
|
108
|
-
return strip(component.render(120));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function renderProgressText(progress: AgentProgress, expanded: boolean, uiTheme: Theme): string {
|
|
112
|
-
const details: TaskToolDetails = {
|
|
113
|
-
projectAgentsDir: null,
|
|
114
|
-
results: [],
|
|
115
|
-
totalDurationMs: 1,
|
|
116
|
-
progress: [progress],
|
|
117
|
-
};
|
|
118
|
-
const component = renderResult(
|
|
119
|
-
{ content: [{ type: "text", text: "Running 1 agent..." }], details },
|
|
120
|
-
{ expanded, isPartial: true },
|
|
121
|
-
uiTheme,
|
|
122
|
-
);
|
|
123
|
-
return strip(component.render(120));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
describe("task live progress rendering", () => {
|
|
127
|
-
let uiTheme: Theme;
|
|
128
|
-
|
|
129
|
-
beforeAll(async () => {
|
|
130
|
-
await Settings.init({ inMemory: true });
|
|
131
|
-
const loaded = await getThemeByName("dark");
|
|
132
|
-
if (!loaded) throw new Error("theme unavailable");
|
|
133
|
-
uiTheme = loaded;
|
|
134
|
-
setThemeInstance(uiTheme);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
afterEach(() => {
|
|
138
|
-
restoreViewportRows();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("caps subagent recent output with the viewport budget instead of a fixed six lines", () => {
|
|
142
|
-
setViewportRows(40);
|
|
143
|
-
const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
|
|
144
|
-
const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
|
|
145
|
-
|
|
146
|
-
expect(text).toContain("line 1");
|
|
147
|
-
expect(text).toContain("line 8");
|
|
148
|
-
expect(text).not.toContain("more lines");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("keeps the newest subagent output when the viewport cap truncates", () => {
|
|
152
|
-
setViewportRows(24);
|
|
153
|
-
const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
|
|
154
|
-
const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
|
|
155
|
-
|
|
156
|
-
expect(text).toContain("… 3 earlier lines");
|
|
157
|
-
expect(text).not.toContain("line 1");
|
|
158
|
-
expect(text).not.toContain("line 3");
|
|
159
|
-
expect(text).toContain("line 4");
|
|
160
|
-
expect(text).toContain("line 8");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("strips bash footer notices from expanded subagent recent output", () => {
|
|
164
|
-
setViewportRows(40);
|
|
165
|
-
const chronological = [
|
|
166
|
-
"line 1",
|
|
167
|
-
"line 2",
|
|
168
|
-
"line 3",
|
|
169
|
-
"line 4",
|
|
170
|
-
"line 5",
|
|
171
|
-
"line 6",
|
|
172
|
-
"Wall time: 0.03 seconds",
|
|
173
|
-
"[raw output: artifact://123]",
|
|
174
|
-
];
|
|
175
|
-
const progress = makeProgress([...chronological].reverse());
|
|
176
|
-
|
|
177
|
-
const expandedText = renderProgressText(progress, true, uiTheme);
|
|
178
|
-
expect(expandedText).toContain("line 1");
|
|
179
|
-
expect(expandedText).toContain("line 6");
|
|
180
|
-
expect(expandedText).not.toContain("Wall time:");
|
|
181
|
-
expect(expandedText).not.toContain("raw output:");
|
|
182
|
-
|
|
183
|
-
const collapsedText = renderProgressText(progress, false, uiTheme);
|
|
184
|
-
expect(collapsedText).not.toContain("line 1");
|
|
185
|
-
expect(collapsedText).not.toContain("raw output:");
|
|
186
|
-
});
|
|
187
|
-
it("sanitizes control sequences from expanded subagent recent output", () => {
|
|
188
|
-
setViewportRows(40);
|
|
189
|
-
const progress = makeProgress(["safe after \x1b[2Kclear", "raw\rprompt"]);
|
|
190
|
-
|
|
191
|
-
const text = renderProgressText(progress, true, uiTheme);
|
|
192
|
-
|
|
193
|
-
expect(text).toContain("safe after clear");
|
|
194
|
-
expect(text).toContain("rawprompt");
|
|
195
|
-
expect(text).not.toContain("\x1b[2K");
|
|
196
|
-
expect(text).not.toContain("\r");
|
|
197
|
-
});
|
|
198
|
-
it("sanitizes control sequences from finalized subagent results", () => {
|
|
199
|
-
const output = JSON.stringify({ "\x1b[2Kkey": "safe value" });
|
|
200
|
-
const details: TaskToolDetails = {
|
|
201
|
-
projectAgentsDir: null,
|
|
202
|
-
results: [
|
|
203
|
-
makeSingleResult(0, {
|
|
204
|
-
id: "Final\x1b[2KAgent",
|
|
205
|
-
description: "description\rtext",
|
|
206
|
-
aborted: true,
|
|
207
|
-
abortReason: "aborted \x1b[2Kreason",
|
|
208
|
-
output,
|
|
209
|
-
}),
|
|
210
|
-
],
|
|
211
|
-
totalDurationMs: 1,
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const text = renderResultText(details, true, uiTheme);
|
|
215
|
-
|
|
216
|
-
expect(text).toContain("FinalAgent");
|
|
217
|
-
expect(text).toContain("descriptiontext");
|
|
218
|
-
expect(text).toContain("aborted reason");
|
|
219
|
-
expect(text).toContain("key");
|
|
220
|
-
expect(text).not.toContain("\x1b[2K");
|
|
221
|
-
expect(text).not.toContain("\r");
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("caps collapsed nested task progress at four rows plus an elision line", () => {
|
|
225
|
-
setViewportRows(40);
|
|
226
|
-
const text = renderProgressText(makeParentWithNestedProgress(6), false, uiTheme);
|
|
227
|
-
|
|
228
|
-
expect(text).not.toContain("Nested1");
|
|
229
|
-
expect(text).not.toContain("Nested2");
|
|
230
|
-
expect(text).toContain("Nested3");
|
|
231
|
-
expect(text).toContain("Nested4");
|
|
232
|
-
expect(text).toContain("Nested5");
|
|
233
|
-
expect(text).toContain("Nested6");
|
|
234
|
-
expect(text).toContain("2 more agents");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("shows every nested task progress row when expanded", () => {
|
|
238
|
-
setViewportRows(40);
|
|
239
|
-
const text = renderProgressText(makeParentWithNestedProgress(6), true, uiTheme);
|
|
240
|
-
|
|
241
|
-
for (let index = 1; index <= 6; index++) {
|
|
242
|
-
expect(text).toContain(`Nested${index}`);
|
|
243
|
-
}
|
|
244
|
-
expect(text).not.toContain("more agents");
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("caps collapsed finalized nested task results and keeps the failed child visible", () => {
|
|
248
|
-
setViewportRows(40);
|
|
249
|
-
const text = renderResultText(makeParentWithNestedResults(6), false, uiTheme);
|
|
250
|
-
|
|
251
|
-
expect(text).toContain("Done6"); // failed child wins a slot despite sorting last
|
|
252
|
-
expect(text).toContain("2 more agents");
|
|
253
|
-
const visibleChildren = [1, 2, 3, 4, 5, 6].filter(index => text.includes(`Done${index}`));
|
|
254
|
-
expect(visibleChildren).toHaveLength(4);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("shows every finalized nested task result when expanded", () => {
|
|
258
|
-
setViewportRows(40);
|
|
259
|
-
const text = renderResultText(makeParentWithNestedResults(6), true, uiTheme);
|
|
260
|
-
|
|
261
|
-
for (let index = 1; index <= 6; index++) {
|
|
262
|
-
expect(text).toContain(`Done${index}`);
|
|
263
|
-
}
|
|
264
|
-
expect(text).not.toContain("more agents");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("does not request spinner ticks for static partial progress", () => {
|
|
268
|
-
expect("animatedPartialResult" in taskToolRenderer).toBe(false);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("renders running progress identically across spinner frames", () => {
|
|
272
|
-
const progress = makeProgress([]);
|
|
273
|
-
const details: TaskToolDetails = {
|
|
274
|
-
projectAgentsDir: null,
|
|
275
|
-
results: [],
|
|
276
|
-
totalDurationMs: 1,
|
|
277
|
-
progress: [progress],
|
|
278
|
-
};
|
|
279
|
-
const render = (spinnerFrame: number) =>
|
|
280
|
-
strip(
|
|
281
|
-
renderResult(
|
|
282
|
-
{ content: [{ type: "text", text: "Running 1 agent..." }], details },
|
|
283
|
-
{ expanded: false, isPartial: true, spinnerFrame },
|
|
284
|
-
uiTheme,
|
|
285
|
-
).render(120),
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
expect(render(0)).toBe(render(1));
|
|
289
|
-
});
|
|
290
|
-
});
|