@dungle-scrubs/tallow 0.8.21 → 0.8.23
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/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +2 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +82 -0
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +68 -1
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-relay.d.ts +40 -7
- package/dist/workspace-transition-relay.d.ts.map +1 -1
- package/dist/workspace-transition-relay.js +81 -16
- package/dist/workspace-transition-relay.js.map +1 -1
- package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
- package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
- package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
- package/extensions/_shared/atomic-write.ts +1 -1
- package/extensions/_shared/bordered-box.ts +102 -0
- package/extensions/_shared/interop-events.ts +5 -0
- package/extensions/_shared/pid-registry.ts +1 -1
- package/extensions/agent-commands-tool/index.ts +4 -1
- package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
- package/extensions/background-task-tool/index.ts +139 -221
- package/extensions/bash-tool-enhanced/index.ts +1 -75
- package/extensions/cd-tool/index.ts +2 -2
- package/extensions/context-fork/spawn.ts +4 -1
- package/extensions/health/index.ts +6 -6
- package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
- package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
- package/extensions/hooks/index.ts +27 -4
- package/extensions/loop/__tests__/loop.test.ts +168 -4
- package/extensions/loop/extension.json +6 -5
- package/extensions/loop/index.ts +242 -31
- package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
- package/extensions/plan-mode-tool/index.ts +103 -41
- package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
- package/extensions/prompt-suggestions/index.ts +41 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
- package/extensions/slash-command-bridge/extension.json +1 -1
- package/extensions/slash-command-bridge/index.ts +230 -116
- package/extensions/subagent-tool/index.ts +2 -2
- package/extensions/subagent-tool/process.ts +4 -5
- package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
- package/extensions/teams-tool/dashboard.ts +3 -5
- package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
- package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
- package/extensions/wezterm-pane-control/index.ts +113 -8
- package/package.json +6 -4
- package/packages/tallow-tui/README.md +51 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
- package/packages/tallow-tui/dist/autocomplete.js +564 -0
- package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
- package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
- package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
- package/packages/tallow-tui/dist/border-styles.js +46 -0
- package/packages/tallow-tui/dist/border-styles.js.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
- package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
- package/packages/tallow-tui/dist/components/box.d.ts +22 -0
- package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/box.js +104 -0
- package/packages/tallow-tui/dist/components/box.js.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
- package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/editor.js +1766 -0
- package/packages/tallow-tui/dist/components/editor.js.map +1 -0
- package/packages/tallow-tui/dist/components/image.d.ts +126 -0
- package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/image.js +245 -0
- package/packages/tallow-tui/dist/components/image.js.map +1 -0
- package/packages/tallow-tui/dist/components/input.d.ts +37 -0
- package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/input.js +439 -0
- package/packages/tallow-tui/dist/components/input.js.map +1 -0
- package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
- package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/loader.js +146 -0
- package/packages/tallow-tui/dist/components/loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.js +633 -0
- package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.js +156 -0
- package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.js +189 -0
- package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.js +23 -0
- package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
- package/packages/tallow-tui/dist/components/text.d.ts +19 -0
- package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/text.js +91 -0
- package/packages/tallow-tui/dist/components/text.js.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
- package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
- package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
- package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
- package/packages/tallow-tui/dist/editor-component.js +2 -0
- package/packages/tallow-tui/dist/editor-component.js.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.js +107 -0
- package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
- package/packages/tallow-tui/dist/index.d.ts +25 -0
- package/packages/tallow-tui/dist/index.d.ts.map +1 -0
- package/packages/tallow-tui/dist/index.js +35 -0
- package/packages/tallow-tui/dist/index.js.map +1 -0
- package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
- package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keybindings.js +114 -0
- package/packages/tallow-tui/dist/keybindings.js.map +1 -0
- package/packages/tallow-tui/dist/keys.d.ts +168 -0
- package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keys.js +971 -0
- package/packages/tallow-tui/dist/keys.js.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.js +44 -0
- package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
- package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.js +460 -0
- package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
- package/packages/tallow-tui/dist/terminal.d.ts +102 -0
- package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal.js +263 -0
- package/packages/tallow-tui/dist/terminal.js.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
- package/packages/tallow-tui/dist/tui.d.ts +239 -0
- package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
- package/packages/tallow-tui/dist/tui.js +1058 -0
- package/packages/tallow-tui/dist/tui.js.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.js +25 -0
- package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
- package/packages/tallow-tui/dist/utils.d.ts +96 -0
- package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
- package/packages/tallow-tui/dist/utils.js +843 -0
- package/packages/tallow-tui/dist/utils.js.map +1 -0
- package/packages/tallow-tui/package.json +24 -0
- package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
- package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
- package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
- package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
- package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
- package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
- package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
- package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
- package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
- package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
- package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
- package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
- package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
- package/packages/tallow-tui/src/autocomplete.ts +716 -0
- package/packages/tallow-tui/src/border-styles.ts +60 -0
- package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
- package/packages/tallow-tui/src/components/box.ts +137 -0
- package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
- package/packages/tallow-tui/src/components/editor.ts +2143 -0
- package/packages/tallow-tui/src/components/image.ts +315 -0
- package/packages/tallow-tui/src/components/input.ts +522 -0
- package/packages/tallow-tui/src/components/loader.ts +187 -0
- package/packages/tallow-tui/src/components/markdown.ts +780 -0
- package/packages/tallow-tui/src/components/select-list.ts +197 -0
- package/packages/tallow-tui/src/components/settings-list.ts +264 -0
- package/packages/tallow-tui/src/components/spacer.ts +28 -0
- package/packages/tallow-tui/src/components/text.ts +113 -0
- package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
- package/packages/tallow-tui/src/editor-component.ts +92 -0
- package/packages/tallow-tui/src/fuzzy.ts +133 -0
- package/packages/tallow-tui/src/index.ts +118 -0
- package/packages/tallow-tui/src/keybindings.ts +183 -0
- package/packages/tallow-tui/src/keys.ts +1189 -0
- package/packages/tallow-tui/src/kill-ring.ts +46 -0
- package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
- package/packages/tallow-tui/src/terminal-image.ts +619 -0
- package/packages/tallow-tui/src/terminal.ts +350 -0
- package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
- package/packages/tallow-tui/src/tui.ts +1336 -0
- package/packages/tallow-tui/src/undo-stack.ts +28 -0
- package/packages/tallow-tui/src/utils.ts +948 -0
- package/packages/tallow-tui/tsconfig.build.json +21 -0
- package/runtime/agent-runner.ts +20 -0
- package/runtime/atomic-write.ts +8 -0
- package/runtime/otel.ts +12 -0
- package/runtime/resolve-module.ts +23 -0
- package/runtime/runtime-path-provider.ts +12 -0
- package/runtime/runtime-provenance.ts +17 -0
- package/runtime/workspace-transition-relay.ts +21 -0
- package/runtime/workspace-transition.ts +29 -0
|
@@ -1,57 +1,142 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for the slash-command-bridge extension.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Focuses on compact deferral, chosen lifecycle boundary, exactly-once guards,
|
|
5
|
+
* and deterministic timer-driven continuation behavior.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
-
import type {
|
|
9
|
+
import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
|
10
|
+
import type {
|
|
11
|
+
ContextUsage,
|
|
12
|
+
ExtensionContext,
|
|
13
|
+
ExtensionUIContext,
|
|
14
|
+
TurnEndEvent,
|
|
15
|
+
} from "@mariozechner/pi-coding-agent";
|
|
10
16
|
import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
import { ManualTimerScheduler } from "../../../test-utils/manual-timer-scheduler.js";
|
|
18
|
+
import slashCommandBridge, {
|
|
19
|
+
resetSlashCommandBridgeStateForTests,
|
|
20
|
+
setSlashCommandBridgeSchedulerForTests,
|
|
21
|
+
} from "../index.js";
|
|
22
|
+
|
|
23
|
+
const ZERO_USAGE: Usage = {
|
|
24
|
+
input: 0,
|
|
25
|
+
output: 0,
|
|
26
|
+
cacheRead: 0,
|
|
27
|
+
cacheWrite: 0,
|
|
28
|
+
totalTokens: 0,
|
|
29
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
30
|
+
};
|
|
14
31
|
|
|
15
32
|
let harness: ExtensionHarness;
|
|
33
|
+
let scheduler: ManualTimerScheduler;
|
|
16
34
|
|
|
17
35
|
beforeEach(async () => {
|
|
36
|
+
scheduler = new ManualTimerScheduler();
|
|
37
|
+
setSlashCommandBridgeSchedulerForTests(scheduler.runtime);
|
|
18
38
|
harness = ExtensionHarness.create();
|
|
19
39
|
await harness.loadExtension(slashCommandBridge);
|
|
20
40
|
});
|
|
21
41
|
|
|
22
|
-
afterEach(
|
|
23
|
-
|
|
24
|
-
type: "session_before_switch",
|
|
25
|
-
reason: "switch",
|
|
26
|
-
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
resetSlashCommandBridgeStateForTests();
|
|
27
44
|
});
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Builds a mock ExtensionContext with overridable methods.
|
|
48
|
+
*
|
|
49
|
+
* @param overrides - Methods/properties to override on the default stub context
|
|
50
|
+
* @returns Mock ExtensionContext
|
|
51
|
+
*/
|
|
52
|
+
function buildContext(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
53
|
+
return {
|
|
54
|
+
ui: {} as ExtensionContext["ui"],
|
|
55
|
+
hasUI: false,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
sessionManager: {} as ExtensionContext["sessionManager"],
|
|
58
|
+
modelRegistry: {} as ExtensionContext["modelRegistry"],
|
|
59
|
+
model: undefined,
|
|
60
|
+
isIdle: () => true,
|
|
61
|
+
abort: () => {},
|
|
62
|
+
hasPendingMessages: () => false,
|
|
63
|
+
shutdown: () => {},
|
|
64
|
+
getContextUsage: () => undefined,
|
|
65
|
+
compact: () => {},
|
|
66
|
+
getSystemPrompt: () => "",
|
|
67
|
+
...overrides,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
30
70
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Creates a realistic assistant turn_end event for compact lifecycle tests.
|
|
73
|
+
*
|
|
74
|
+
* @param stopReason - Assistant stop reason for the completed turn
|
|
75
|
+
* @returns TurnEnd event payload
|
|
76
|
+
*/
|
|
77
|
+
function buildAssistantTurnEnd(stopReason: AssistantMessage["stopReason"]): TurnEndEvent {
|
|
78
|
+
return {
|
|
79
|
+
type: "turn_end",
|
|
80
|
+
turnIndex: 0,
|
|
81
|
+
message: {
|
|
82
|
+
role: "assistant",
|
|
83
|
+
content: [],
|
|
84
|
+
api: "anthropic-messages",
|
|
85
|
+
provider: "mock",
|
|
86
|
+
model: "mock-model",
|
|
87
|
+
stopReason,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
usage: { ...ZERO_USAGE },
|
|
90
|
+
},
|
|
91
|
+
toolResults: stopReason === "toolUse" ? [buildCompactToolResult()] : [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
35
94
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Builds the compact tool result payload recorded on the tool-use turn.
|
|
97
|
+
*
|
|
98
|
+
* @returns Tool result message for `run_slash_command({ command: "compact" })`
|
|
99
|
+
*/
|
|
100
|
+
function buildCompactToolResult(): ToolResultMessage<{ command: string }> {
|
|
101
|
+
return {
|
|
102
|
+
role: "toolResult",
|
|
103
|
+
toolCallId: "mock-tool-call",
|
|
104
|
+
toolName: "run_slash_command",
|
|
105
|
+
content: [
|
|
106
|
+
{ type: "text", text: "Session compaction will begin after this response completes." },
|
|
107
|
+
],
|
|
108
|
+
details: { command: "compact" },
|
|
109
|
+
isError: false,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
40
113
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Executes the registered slash-command tool with the provided context.
|
|
116
|
+
*
|
|
117
|
+
* @param params - Tool parameters
|
|
118
|
+
* @param ctx - Extension context for the execution
|
|
119
|
+
* @returns Tool execution result
|
|
120
|
+
*/
|
|
121
|
+
async function executeTool(params: { command: string }, ctx?: ExtensionContext) {
|
|
122
|
+
const tool = harness.tools.get("run_slash_command");
|
|
123
|
+
if (!tool) {
|
|
124
|
+
throw new Error("run_slash_command tool not registered");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return tool.execute("test-call-id", params, undefined, undefined, ctx ?? buildContext());
|
|
128
|
+
}
|
|
47
129
|
|
|
48
|
-
|
|
130
|
+
describe("registration", () => {
|
|
131
|
+
test("registers run_slash_command and lifecycle handlers", () => {
|
|
132
|
+
expect(harness.tools.has("run_slash_command")).toBe(true);
|
|
49
133
|
expect(harness.handlers.has("before_agent_start")).toBe(true);
|
|
134
|
+
expect(harness.handlers.has("turn_end")).toBe(true);
|
|
135
|
+
expect(harness.handlers.has("turn_start")).toBe(true);
|
|
136
|
+
expect(harness.handlers.has("session_before_switch")).toBe(true);
|
|
50
137
|
});
|
|
51
138
|
});
|
|
52
139
|
|
|
53
|
-
// ── Command execution: show-system-prompt ────────────────────────────────────
|
|
54
|
-
|
|
55
140
|
describe("show-system-prompt", () => {
|
|
56
141
|
test("returns the current system prompt", async () => {
|
|
57
142
|
const systemPrompt = "You are a helpful assistant with custom instructions.";
|
|
@@ -60,101 +145,71 @@ describe("show-system-prompt", () => {
|
|
|
60
145
|
const result = await executeTool({ command: "show-system-prompt" }, ctx);
|
|
61
146
|
|
|
62
147
|
expect(result.content[0]).toEqual({ type: "text", text: systemPrompt });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("includes prompt length in details", async () => {
|
|
66
|
-
const systemPrompt = "Short prompt.";
|
|
67
|
-
const ctx = buildContext({ getSystemPrompt: () => systemPrompt });
|
|
68
|
-
|
|
69
|
-
const result = await executeTool({ command: "show-system-prompt" }, ctx);
|
|
70
|
-
|
|
71
148
|
expect(result.details).toEqual({ command: "show-system-prompt", length: systemPrompt.length });
|
|
72
149
|
});
|
|
73
|
-
|
|
74
|
-
test("handles empty system prompt", async () => {
|
|
75
|
-
const ctx = buildContext({ getSystemPrompt: () => "" });
|
|
76
|
-
|
|
77
|
-
const result = await executeTool({ command: "show-system-prompt" }, ctx);
|
|
78
|
-
|
|
79
|
-
expect(result.content[0]).toEqual({ type: "text", text: "" });
|
|
80
|
-
expect(result.isError).toBeUndefined();
|
|
81
|
-
});
|
|
82
150
|
});
|
|
83
151
|
|
|
84
|
-
// ── Command execution: context ───────────────────────────────────────────────
|
|
85
|
-
|
|
86
152
|
describe("context", () => {
|
|
87
153
|
test("returns formatted context usage", async () => {
|
|
88
154
|
const usage: ContextUsage = { tokens: 45000, contextWindow: 200000 };
|
|
89
155
|
const ctx = buildContext({ getContextUsage: () => usage });
|
|
90
156
|
|
|
91
157
|
const result = await executeTool({ command: "context" }, ctx);
|
|
92
|
-
|
|
93
158
|
const text = result.content[0];
|
|
159
|
+
|
|
94
160
|
expect(text).toBeDefined();
|
|
95
161
|
if (text?.type === "text") {
|
|
96
162
|
expect(text.text).toContain("45,000");
|
|
97
163
|
expect(text.text).toContain("200,000");
|
|
98
164
|
expect(text.text).toContain("22.5%");
|
|
99
|
-
expect(text.text).toContain("155,000"); // free tokens
|
|
100
165
|
}
|
|
101
166
|
});
|
|
102
167
|
|
|
103
|
-
test("
|
|
104
|
-
const
|
|
105
|
-
const ctx = buildContext({ getContextUsage: () => usage });
|
|
106
|
-
|
|
107
|
-
const result = await executeTool({ command: "context" }, ctx);
|
|
108
|
-
|
|
109
|
-
expect(result.details).toEqual({
|
|
110
|
-
command: "context",
|
|
111
|
-
tokens: 10000,
|
|
112
|
-
contextWindow: 100000,
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("returns error when no usage data available", async () => {
|
|
117
|
-
const ctx = buildContext({ getContextUsage: () => undefined });
|
|
118
|
-
|
|
119
|
-
const result = await executeTool({ command: "context" }, ctx);
|
|
168
|
+
test("returns error when usage data is unavailable", async () => {
|
|
169
|
+
const result = await executeTool({ command: "context" }, buildContext());
|
|
120
170
|
|
|
121
171
|
expect(result.isError).toBe(true);
|
|
122
|
-
|
|
123
|
-
if (text?.type === "text") {
|
|
124
|
-
expect(text.text).toContain("No context usage data");
|
|
125
|
-
}
|
|
172
|
+
expect(result.details).toEqual({ command: "context", error: "no_usage_data" });
|
|
126
173
|
});
|
|
174
|
+
});
|
|
127
175
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const result = await executeTool({ command: "context" }, ctx);
|
|
176
|
+
describe("error handling", () => {
|
|
177
|
+
test("rejects unknown commands", async () => {
|
|
178
|
+
const result = await executeTool({ command: "reboot" }, buildContext());
|
|
133
179
|
|
|
134
180
|
expect(result.isError).toBe(true);
|
|
135
|
-
expect(result.details).toEqual({ command: "context", error: "no_usage_data" });
|
|
136
181
|
const text = result.content[0];
|
|
137
182
|
if (text?.type === "text") {
|
|
138
|
-
expect(text.text).toContain("
|
|
139
|
-
expect(text.text).
|
|
140
|
-
expect(text.text).not.toContain("0.0%");
|
|
183
|
+
expect(text.text).toContain("Unknown command");
|
|
184
|
+
expect(text.text).toContain("reboot");
|
|
141
185
|
}
|
|
142
186
|
});
|
|
187
|
+
});
|
|
143
188
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
189
|
+
describe("context injection", () => {
|
|
190
|
+
test("injects hidden context listing bridged commands", async () => {
|
|
191
|
+
const results = await harness.fireEvent("before_agent_start", {
|
|
192
|
+
type: "before_agent_start",
|
|
193
|
+
prompt: "hello",
|
|
194
|
+
systemPrompt: "",
|
|
195
|
+
});
|
|
147
196
|
|
|
148
|
-
const result =
|
|
197
|
+
const result = results.find((entry) => entry != null) as
|
|
198
|
+
| {
|
|
199
|
+
message: { content: string; customType: string; display: boolean };
|
|
200
|
+
}
|
|
201
|
+
| undefined;
|
|
149
202
|
|
|
150
|
-
expect(result.
|
|
203
|
+
expect(result?.message.customType).toBe("slash-command-bridge-context");
|
|
204
|
+
expect(result?.message.display).toBe(false);
|
|
205
|
+
expect(result?.message.content).toContain("/show-system-prompt");
|
|
206
|
+
expect(result?.message.content).toContain("/context");
|
|
207
|
+
expect(result?.message.content).toContain("/compact");
|
|
151
208
|
});
|
|
152
209
|
});
|
|
153
210
|
|
|
154
|
-
// ── Command execution: compact ───────────────────────────────────────────────
|
|
155
|
-
|
|
156
211
|
describe("compact", () => {
|
|
157
|
-
test("
|
|
212
|
+
test("defers compact instead of calling ctx.compact inline", async () => {
|
|
158
213
|
let compactCalled = false;
|
|
159
214
|
const ctx = buildContext({
|
|
160
215
|
compact: () => {
|
|
@@ -162,17 +217,10 @@ describe("compact", () => {
|
|
|
162
217
|
},
|
|
163
218
|
});
|
|
164
219
|
|
|
165
|
-
await executeTool({ command: "compact" }, ctx);
|
|
166
|
-
|
|
167
|
-
expect(compactCalled).toBe(false);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("returns message instructing model to finish response", async () => {
|
|
171
|
-
const ctx = buildContext({ compact: () => {} });
|
|
172
|
-
|
|
173
220
|
const result = await executeTool({ command: "compact" }, ctx);
|
|
174
221
|
|
|
175
|
-
expect(
|
|
222
|
+
expect(compactCalled).toBe(false);
|
|
223
|
+
expect(result.details).toEqual({ command: "compact" });
|
|
176
224
|
const text = result.content[0];
|
|
177
225
|
if (text?.type === "text") {
|
|
178
226
|
expect(text.text).toContain("compaction will begin after this response");
|
|
@@ -180,686 +228,234 @@ describe("compact", () => {
|
|
|
180
228
|
}
|
|
181
229
|
});
|
|
182
230
|
|
|
183
|
-
test("
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const result = await executeTool({ command: "compact" }, ctx);
|
|
187
|
-
|
|
188
|
-
expect(result.details).toEqual({ command: "compact" });
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("agent_end hook triggers deferred compact with callbacks", async () => {
|
|
231
|
+
test("waits through the tool-use turn and compacts on the following assistant turn_end", async () => {
|
|
232
|
+
let compactCalls = 0;
|
|
192
233
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
193
|
-
const
|
|
194
|
-
const agentEndCtx = buildContext({
|
|
234
|
+
const ctx = buildContext({
|
|
195
235
|
compact: (options) => {
|
|
236
|
+
compactCalls++;
|
|
196
237
|
compactOptions = options;
|
|
197
238
|
},
|
|
198
239
|
});
|
|
199
240
|
|
|
200
|
-
|
|
201
|
-
await
|
|
202
|
-
|
|
203
|
-
// agent_end fires — should trigger compact
|
|
204
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
241
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
242
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
243
|
+
expect(compactCalls).toBe(0);
|
|
205
244
|
|
|
206
|
-
|
|
245
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
246
|
+
expect(compactCalls).toBe(1);
|
|
207
247
|
expect(typeof compactOptions?.onComplete).toBe("function");
|
|
208
248
|
expect(typeof compactOptions?.onError).toBe("function");
|
|
209
|
-
|
|
210
|
-
// Clean up compact progress interval to avoid cross-test leakage.
|
|
211
|
-
compactOptions?.onError?.();
|
|
212
249
|
});
|
|
213
250
|
|
|
214
|
-
test("
|
|
215
|
-
let
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const agentEndCtx = buildContext({
|
|
220
|
-
hasUI: true,
|
|
221
|
-
ui: {
|
|
222
|
-
setWorkingMessage: (message?: string) => {
|
|
223
|
-
workingMessages.push(message);
|
|
224
|
-
},
|
|
225
|
-
setStatus: (key: string, text?: string) => {
|
|
226
|
-
statusUpdates.push({ key, text });
|
|
227
|
-
},
|
|
228
|
-
} as ExtensionContext["ui"],
|
|
229
|
-
compact: (options) => {
|
|
230
|
-
compactOptions = options;
|
|
251
|
+
test("consumes the pending request exactly once", async () => {
|
|
252
|
+
let compactCalls = 0;
|
|
253
|
+
const ctx = buildContext({
|
|
254
|
+
compact: () => {
|
|
255
|
+
compactCalls++;
|
|
231
256
|
},
|
|
232
257
|
});
|
|
233
258
|
|
|
234
|
-
await executeTool({ command: "compact" },
|
|
235
|
-
await harness.fireEvent("
|
|
259
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
260
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
261
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
262
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
236
263
|
|
|
237
|
-
expect(
|
|
238
|
-
expect(statusUpdates[0]?.key).toBe("compact");
|
|
239
|
-
expect(statusUpdates[0]?.text).toContain("compacting · 0s");
|
|
240
|
-
|
|
241
|
-
await sleep(1100);
|
|
242
|
-
|
|
243
|
-
const hasElapsedUpdate = statusUpdates.some((update) =>
|
|
244
|
-
update.text?.includes("compacting · 1s")
|
|
245
|
-
);
|
|
246
|
-
expect(hasElapsedUpdate).toBe(true);
|
|
247
|
-
|
|
248
|
-
compactOptions?.onError?.();
|
|
264
|
+
expect(compactCalls).toBe(1);
|
|
249
265
|
});
|
|
250
266
|
|
|
251
|
-
test("
|
|
267
|
+
test("drives heartbeat and continuation timers deterministically", async () => {
|
|
252
268
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
269
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
253
270
|
const workingMessages: Array<string | undefined> = [];
|
|
254
|
-
const
|
|
255
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
256
|
-
const agentEndCtx = buildContext({
|
|
271
|
+
const ctx = buildContext({
|
|
257
272
|
hasUI: true,
|
|
258
273
|
ui: {
|
|
274
|
+
setWidget: (key: string, content?: string[]) => {
|
|
275
|
+
widgetUpdates.push({ key, content });
|
|
276
|
+
},
|
|
259
277
|
setWorkingMessage: (message?: string) => {
|
|
260
278
|
workingMessages.push(message);
|
|
261
279
|
},
|
|
262
|
-
|
|
263
|
-
statusUpdates.push({ key, text });
|
|
264
|
-
},
|
|
265
|
-
} as ExtensionContext["ui"],
|
|
280
|
+
} as ExtensionUIContext,
|
|
266
281
|
compact: (options) => {
|
|
267
282
|
compactOptions = options;
|
|
268
283
|
},
|
|
269
284
|
isIdle: () => true,
|
|
270
285
|
});
|
|
271
286
|
|
|
272
|
-
await executeTool({ command: "compact" },
|
|
273
|
-
await harness.fireEvent("
|
|
274
|
-
await
|
|
275
|
-
|
|
276
|
-
compactOptions?.onComplete?.();
|
|
277
|
-
expect(workingMessages.at(-1)).toBe("Resuming task…");
|
|
278
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: "⏳ resuming" });
|
|
279
|
-
|
|
280
|
-
const updatesAfterComplete = statusUpdates.length;
|
|
281
|
-
await sleep(1200);
|
|
282
|
-
expect(statusUpdates).toHaveLength(updatesAfterComplete);
|
|
283
|
-
});
|
|
287
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
288
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
289
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
284
290
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
compact: (options) => {
|
|
290
|
-
compactOptions = options;
|
|
291
|
-
},
|
|
292
|
-
isIdle: () => true,
|
|
291
|
+
expect(workingMessages[0]).toBe("Compacting session…");
|
|
292
|
+
expect(widgetUpdates[0]).toEqual({
|
|
293
|
+
key: "compact-progress",
|
|
294
|
+
content: ["🧹 ⠋ Compacting session · 0s"],
|
|
293
295
|
});
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
+
scheduler.advanceBy(1000);
|
|
298
|
+
expect(widgetUpdates.at(-1)).toEqual({
|
|
299
|
+
key: "compact-progress",
|
|
300
|
+
content: ["🧹 ⠙ Compacting session · 1s"],
|
|
301
|
+
});
|
|
297
302
|
|
|
298
|
-
// Trigger onComplete and wait for the setTimeout(200) to fire
|
|
299
303
|
compactOptions?.onComplete?.();
|
|
300
|
-
|
|
304
|
+
expect(workingMessages.at(-1)).toBe("Resuming task…");
|
|
305
|
+
expect(widgetUpdates.at(-1)).toEqual({
|
|
306
|
+
key: "compact-progress",
|
|
307
|
+
content: ["⏳ Resuming after compaction…"],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
scheduler.advanceBy(199);
|
|
311
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
312
|
+
scheduler.advanceBy(1);
|
|
301
313
|
|
|
302
|
-
const continuation = harness.sentMessages.find(
|
|
303
|
-
|
|
314
|
+
const continuation = harness.sentMessages.find(
|
|
315
|
+
(message) => message.customType === "compact-continue"
|
|
316
|
+
);
|
|
304
317
|
expect(continuation?.display).toBe(false);
|
|
305
318
|
expect(continuation?.options?.triggerTurn).toBe(true);
|
|
306
319
|
expect(continuation?.content).toContain("compaction is complete");
|
|
307
320
|
});
|
|
308
321
|
|
|
309
|
-
test("
|
|
310
|
-
// Previously, onComplete short-circuited when hasCompactionQueuedMessages()
|
|
311
|
-
// returned true. This caused orphaned session steering messages because the
|
|
312
|
-
// method's false positive (checking session steering too) prevented the
|
|
313
|
-
// continuation timer from firing. Now the timer always fires — safety nets
|
|
314
|
-
// (turn_start cancellation, isIdle() check) prevent duplicate prompts.
|
|
315
|
-
// See plan 160.
|
|
322
|
+
test("turn_start cancels the delayed continuation and clears the inline widget", async () => {
|
|
316
323
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
317
|
-
const
|
|
318
|
-
const
|
|
324
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
325
|
+
const ctx = buildContext({
|
|
319
326
|
hasUI: true,
|
|
320
327
|
ui: {
|
|
328
|
+
setWidget: (key: string, content?: string[]) => {
|
|
329
|
+
widgetUpdates.push({ key, content });
|
|
330
|
+
},
|
|
321
331
|
setWorkingMessage: () => {},
|
|
322
|
-
|
|
323
|
-
// Even with hasCompactionQueuedMessages exposed, onComplete
|
|
324
|
-
// no longer checks it.
|
|
325
|
-
hasCompactionQueuedMessages: () => true,
|
|
326
|
-
} as unknown as ExtensionContext["ui"],
|
|
332
|
+
} as ExtensionUIContext,
|
|
327
333
|
compact: (options) => {
|
|
328
334
|
compactOptions = options;
|
|
329
335
|
},
|
|
330
336
|
isIdle: () => true,
|
|
331
337
|
});
|
|
332
338
|
|
|
333
|
-
await executeTool({ command: "compact" },
|
|
334
|
-
await harness.fireEvent("
|
|
335
|
-
|
|
339
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
340
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
341
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
336
342
|
compactOptions?.onComplete?.();
|
|
337
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
338
343
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
expect(
|
|
344
|
+
await harness.fireEvent("turn_start", { type: "turn_start", turnIndex: 0, timestamp: 0 }, ctx);
|
|
345
|
+
scheduler.advanceBy(200);
|
|
346
|
+
|
|
347
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
348
|
+
expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
|
|
343
349
|
});
|
|
344
350
|
|
|
345
|
-
test("
|
|
351
|
+
test("skips continuation and clears indicators when the session is no longer idle", async () => {
|
|
346
352
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
347
|
-
const
|
|
353
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
348
354
|
const workingMessages: Array<string | undefined> = [];
|
|
349
|
-
const
|
|
350
|
-
const agentEndCtx = buildContext({
|
|
355
|
+
const ctx = buildContext({
|
|
351
356
|
hasUI: true,
|
|
352
357
|
ui: {
|
|
358
|
+
setWidget: (key: string, content?: string[]) => {
|
|
359
|
+
widgetUpdates.push({ key, content });
|
|
360
|
+
},
|
|
353
361
|
setWorkingMessage: (message?: string) => {
|
|
354
362
|
workingMessages.push(message);
|
|
355
363
|
},
|
|
356
|
-
|
|
357
|
-
statusUpdates.push({ key, text });
|
|
358
|
-
},
|
|
359
|
-
} as ExtensionContext["ui"],
|
|
364
|
+
} as ExtensionUIContext,
|
|
360
365
|
compact: (options) => {
|
|
361
366
|
compactOptions = options;
|
|
362
367
|
},
|
|
363
368
|
isIdle: () => false,
|
|
364
369
|
});
|
|
365
370
|
|
|
366
|
-
await executeTool({ command: "compact" },
|
|
367
|
-
await harness.fireEvent("
|
|
368
|
-
|
|
371
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
372
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
373
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
369
374
|
compactOptions?.onComplete?.();
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const continuation = harness.sentMessages.find((m) => m.customType === "compact-continue");
|
|
373
|
-
expect(continuation).toBeUndefined();
|
|
375
|
+
scheduler.advanceBy(200);
|
|
374
376
|
|
|
375
|
-
|
|
376
|
-
expect(
|
|
377
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
378
|
+
expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
|
|
377
379
|
expect(workingMessages.at(-1)).toBeUndefined();
|
|
378
380
|
});
|
|
379
381
|
|
|
380
|
-
test("onError
|
|
382
|
+
test("onError clears compact UI and sends no continuation", async () => {
|
|
381
383
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
const agentEndCtx = buildContext({
|
|
385
|
-
hasUI: true,
|
|
386
|
-
ui: {
|
|
387
|
-
setWorkingMessage: () => {},
|
|
388
|
-
setStatus: (key: string, text?: string) => {
|
|
389
|
-
statusUpdates.push({ key, text });
|
|
390
|
-
},
|
|
391
|
-
} as ExtensionContext["ui"],
|
|
392
|
-
compact: (options) => {
|
|
393
|
-
compactOptions = options;
|
|
394
|
-
},
|
|
395
|
-
isIdle: () => true,
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
399
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
400
|
-
await sleep(1100);
|
|
401
|
-
|
|
402
|
-
compactOptions?.onError?.();
|
|
403
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: undefined });
|
|
404
|
-
|
|
405
|
-
const updatesAfterError = statusUpdates.length;
|
|
406
|
-
await sleep(1200);
|
|
407
|
-
expect(statusUpdates).toHaveLength(updatesAfterError);
|
|
408
|
-
|
|
409
|
-
const continuation = harness.sentMessages.find((m) => m.customType === "compact-continue");
|
|
410
|
-
expect(continuation).toBeUndefined();
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
test("agent_end hook is a no-op when no compact is pending", async () => {
|
|
414
|
-
let compactCalled = false;
|
|
384
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
385
|
+
const workingMessages: Array<string | undefined> = [];
|
|
415
386
|
const ctx = buildContext({
|
|
416
|
-
compact: () => {
|
|
417
|
-
compactCalled = true;
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// Fire agent_end without a preceding compact tool call
|
|
422
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, ctx);
|
|
423
|
-
|
|
424
|
-
expect(compactCalled).toBe(false);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test("turn_start cancels continuation timer before it fires", async () => {
|
|
428
|
-
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
429
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
430
|
-
const agentEndCtx = buildContext({
|
|
431
387
|
hasUI: true,
|
|
432
388
|
ui: {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
} as ExtensionContext["ui"],
|
|
436
|
-
compact: (options) => {
|
|
437
|
-
compactOptions = options;
|
|
438
|
-
},
|
|
439
|
-
isIdle: () => true,
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
443
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
444
|
-
|
|
445
|
-
// Trigger onComplete — starts the 200ms timer
|
|
446
|
-
compactOptions?.onComplete?.();
|
|
447
|
-
|
|
448
|
-
// Fire turn_start before the timer expires (simulates flushCompactionQueue
|
|
449
|
-
// prompting the agent first)
|
|
450
|
-
const turnCtx = buildContext({
|
|
451
|
-
hasUI: true,
|
|
452
|
-
ui: {
|
|
453
|
-
setStatus: () => {},
|
|
454
|
-
} as ExtensionContext["ui"],
|
|
455
|
-
});
|
|
456
|
-
await harness.fireEvent("turn_start", { type: "turn_start" }, turnCtx);
|
|
457
|
-
|
|
458
|
-
// Wait longer than the 200ms timeout
|
|
459
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
460
|
-
|
|
461
|
-
// Timer was cancelled — no duplicate continuation message sent
|
|
462
|
-
const continuation = harness.sentMessages.find((m) => m.customType === "compact-continue");
|
|
463
|
-
expect(continuation).toBeUndefined();
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test("turn_start clears footer status when resuming after compact", async () => {
|
|
467
|
-
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
468
|
-
const statusUpdates: Array<{ key: string; text: string | undefined }> = [];
|
|
469
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
470
|
-
const agentEndCtx = buildContext({
|
|
471
|
-
hasUI: true,
|
|
472
|
-
ui: {
|
|
473
|
-
setWorkingMessage: () => {},
|
|
474
|
-
setStatus: (key: string, text?: string) => {
|
|
475
|
-
statusUpdates.push({ key, text });
|
|
476
|
-
},
|
|
477
|
-
} as ExtensionContext["ui"],
|
|
478
|
-
compact: (options) => {
|
|
479
|
-
compactOptions = options;
|
|
480
|
-
},
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
484
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
485
|
-
compactOptions?.onComplete?.();
|
|
486
|
-
|
|
487
|
-
// Resuming status should be set
|
|
488
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: "⏳ resuming" });
|
|
489
|
-
|
|
490
|
-
// turn_start fires — should clear the footer status
|
|
491
|
-
const turnCtx = buildContext({
|
|
492
|
-
hasUI: true,
|
|
493
|
-
ui: {
|
|
494
|
-
setStatus: (key: string, text?: string) => {
|
|
495
|
-
statusUpdates.push({ key, text });
|
|
389
|
+
setWidget: (key: string, content?: string[]) => {
|
|
390
|
+
widgetUpdates.push({ key, content });
|
|
496
391
|
},
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
await harness.fireEvent("turn_start", { type: "turn_start" }, turnCtx);
|
|
500
|
-
|
|
501
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: undefined });
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
test("turn_start is a no-op when not resuming after compact", async () => {
|
|
505
|
-
const statusUpdates: Array<{ key: string; text: string | undefined }> = [];
|
|
506
|
-
const turnCtx = buildContext({
|
|
507
|
-
hasUI: true,
|
|
508
|
-
ui: {
|
|
509
|
-
setStatus: (key: string, text?: string) => {
|
|
510
|
-
statusUpdates.push({ key, text });
|
|
511
|
-
},
|
|
512
|
-
} as ExtensionContext["ui"],
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
// Fire turn_start without any preceding compaction
|
|
516
|
-
await harness.fireEvent("turn_start", { type: "turn_start" }, turnCtx);
|
|
517
|
-
|
|
518
|
-
// No status updates should have been made
|
|
519
|
-
expect(statusUpdates).toHaveLength(0);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test("session_before_switch clears active compact heartbeat state", async () => {
|
|
523
|
-
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
524
|
-
const statusUpdates: Array<{ key: string; text: string | undefined }> = [];
|
|
525
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
526
|
-
const agentEndCtx = buildContext({
|
|
527
|
-
hasUI: true,
|
|
528
|
-
ui: {
|
|
529
|
-
setWorkingMessage: () => {},
|
|
530
|
-
setStatus: (key: string, text?: string) => {
|
|
531
|
-
statusUpdates.push({ key, text });
|
|
392
|
+
setWorkingMessage: (message?: string) => {
|
|
393
|
+
workingMessages.push(message);
|
|
532
394
|
},
|
|
533
|
-
} as
|
|
395
|
+
} as ExtensionUIContext,
|
|
534
396
|
compact: (options) => {
|
|
535
397
|
compactOptions = options;
|
|
536
398
|
},
|
|
399
|
+
isIdle: () => true,
|
|
537
400
|
});
|
|
538
401
|
|
|
539
|
-
await executeTool({ command: "compact" },
|
|
540
|
-
await harness.fireEvent("
|
|
541
|
-
await
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const switchCtx = buildContext({
|
|
547
|
-
hasUI: true,
|
|
548
|
-
ui: {
|
|
549
|
-
setStatus: (key: string, text?: string) => {
|
|
550
|
-
statusUpdates.push({ key, text });
|
|
551
|
-
},
|
|
552
|
-
} as ExtensionContext["ui"],
|
|
553
|
-
});
|
|
554
|
-
await harness.fireEvent(
|
|
555
|
-
"session_before_switch",
|
|
556
|
-
{ type: "session_before_switch", reason: "switch" },
|
|
557
|
-
switchCtx
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: undefined });
|
|
402
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
403
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
404
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
405
|
+
scheduler.advanceBy(1000);
|
|
406
|
+
compactOptions?.onError?.(new Error("boom"));
|
|
407
|
+
scheduler.advanceBy(200);
|
|
561
408
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
expect(
|
|
409
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
410
|
+
expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
|
|
411
|
+
expect(workingMessages.at(-1)).toBeUndefined();
|
|
565
412
|
});
|
|
566
413
|
|
|
567
|
-
test("session_before_switch clears
|
|
414
|
+
test("session_before_switch clears pending compact, timers, and UI state", async () => {
|
|
415
|
+
let compactCalls = 0;
|
|
568
416
|
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
const
|
|
417
|
+
const widgetUpdates: Array<{ key: string; content: string[] | undefined }> = [];
|
|
418
|
+
const workingMessages: Array<string | undefined> = [];
|
|
419
|
+
const ctx = buildContext({
|
|
572
420
|
hasUI: true,
|
|
573
421
|
ui: {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
statusUpdates.push({ key, text });
|
|
422
|
+
setWidget: (key: string, content?: string[]) => {
|
|
423
|
+
widgetUpdates.push({ key, content });
|
|
577
424
|
},
|
|
578
|
-
|
|
425
|
+
setWorkingMessage: (message?: string) => {
|
|
426
|
+
workingMessages.push(message);
|
|
427
|
+
},
|
|
428
|
+
} as ExtensionUIContext,
|
|
579
429
|
compact: (options) => {
|
|
430
|
+
compactCalls++;
|
|
580
431
|
compactOptions = options;
|
|
581
432
|
},
|
|
433
|
+
isIdle: () => true,
|
|
582
434
|
});
|
|
583
435
|
|
|
584
|
-
await executeTool({ command: "compact" },
|
|
585
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
586
|
-
compactOptions?.onComplete?.();
|
|
587
|
-
|
|
588
|
-
// Resuming status should be set
|
|
589
|
-
expect(statusUpdates.at(-1)).toEqual({ key: "compact", text: "⏳ resuming" });
|
|
590
|
-
|
|
591
|
-
// Session switch fires — should clear resuming state
|
|
592
|
-
const switchCtx = buildContext({
|
|
593
|
-
hasUI: true,
|
|
594
|
-
ui: {
|
|
595
|
-
setStatus: (key: string, text?: string) => {
|
|
596
|
-
statusUpdates.push({ key, text });
|
|
597
|
-
},
|
|
598
|
-
} as ExtensionContext["ui"],
|
|
599
|
-
});
|
|
436
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
600
437
|
await harness.fireEvent(
|
|
601
438
|
"session_before_switch",
|
|
602
439
|
{ type: "session_before_switch", reason: "switch" },
|
|
603
|
-
|
|
440
|
+
ctx
|
|
604
441
|
);
|
|
442
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
443
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
444
|
+
expect(compactCalls).toBe(0);
|
|
605
445
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const turnStatusUpdates: Array<{ key: string; text: string | undefined }> = [];
|
|
610
|
-
const turnCtx = buildContext({
|
|
611
|
-
hasUI: true,
|
|
612
|
-
ui: {
|
|
613
|
-
setStatus: (key: string, text?: string) => {
|
|
614
|
-
turnStatusUpdates.push({ key, text });
|
|
615
|
-
},
|
|
616
|
-
} as ExtensionContext["ui"],
|
|
617
|
-
});
|
|
618
|
-
await harness.fireEvent("turn_start", { type: "turn_start" }, turnCtx);
|
|
619
|
-
|
|
620
|
-
expect(turnStatusUpdates).toHaveLength(0);
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
test("session_before_switch cancels continuation timer", async () => {
|
|
624
|
-
let compactOptions: Parameters<ExtensionContext["compact"]>[0];
|
|
625
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
626
|
-
const agentEndCtx = buildContext({
|
|
627
|
-
hasUI: true,
|
|
628
|
-
ui: {
|
|
629
|
-
setWorkingMessage: () => {},
|
|
630
|
-
setStatus: () => {},
|
|
631
|
-
} as ExtensionContext["ui"],
|
|
632
|
-
compact: (options) => {
|
|
633
|
-
compactOptions = options;
|
|
634
|
-
},
|
|
635
|
-
isIdle: () => true,
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
639
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
640
|
-
|
|
641
|
-
// Trigger onComplete — starts the 200ms timer
|
|
446
|
+
await executeTool({ command: "compact" }, buildContext());
|
|
447
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("toolUse"), ctx);
|
|
448
|
+
await harness.fireEvent("turn_end", buildAssistantTurnEnd("stop"), ctx);
|
|
642
449
|
compactOptions?.onComplete?.();
|
|
643
|
-
|
|
644
|
-
// Session switch fires before timer expires
|
|
645
|
-
const switchCtx = buildContext({
|
|
646
|
-
hasUI: true,
|
|
647
|
-
ui: {
|
|
648
|
-
setStatus: () => {},
|
|
649
|
-
} as ExtensionContext["ui"],
|
|
650
|
-
});
|
|
651
450
|
await harness.fireEvent(
|
|
652
451
|
"session_before_switch",
|
|
653
452
|
{ type: "session_before_switch", reason: "switch" },
|
|
654
|
-
|
|
453
|
+
ctx
|
|
655
454
|
);
|
|
455
|
+
scheduler.advanceBy(200);
|
|
656
456
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// Timer was cancelled — no continuation message sent
|
|
661
|
-
const continuation = harness.sentMessages.find((m) => m.customType === "compact-continue");
|
|
662
|
-
expect(continuation).toBeUndefined();
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
test("repeated compact lifecycle does not leave duplicate heartbeat intervals", async () => {
|
|
666
|
-
const originalSetInterval = globalThis.setInterval;
|
|
667
|
-
const originalClearInterval = globalThis.clearInterval;
|
|
668
|
-
const createdHandles: unknown[] = [];
|
|
669
|
-
const clearedHandles: unknown[] = [];
|
|
670
|
-
let handleIndex = 0;
|
|
671
|
-
|
|
672
|
-
globalThis.setInterval = ((callback: Parameters<typeof setInterval>[0], _ms?: number) => {
|
|
673
|
-
void callback;
|
|
674
|
-
handleIndex += 1;
|
|
675
|
-
const handle = { id: handleIndex };
|
|
676
|
-
createdHandles.push(handle);
|
|
677
|
-
return handle as unknown as ReturnType<typeof setInterval>;
|
|
678
|
-
}) as typeof setInterval;
|
|
679
|
-
globalThis.clearInterval = ((handle?: ReturnType<typeof setInterval>) => {
|
|
680
|
-
clearedHandles.push(handle);
|
|
681
|
-
}) as typeof clearInterval;
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
685
|
-
const agentEndCtx = buildContext({
|
|
686
|
-
hasUI: true,
|
|
687
|
-
ui: {
|
|
688
|
-
setWorkingMessage: () => {},
|
|
689
|
-
setStatus: () => {},
|
|
690
|
-
} as ExtensionContext["ui"],
|
|
691
|
-
compact: () => {},
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
695
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
696
|
-
|
|
697
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
698
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
699
|
-
|
|
700
|
-
await harness.fireEvent("session_before_switch", {
|
|
701
|
-
type: "session_before_switch",
|
|
702
|
-
reason: "switch",
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
expect(createdHandles).toHaveLength(2);
|
|
706
|
-
expect(clearedHandles).toContain(createdHandles[0]);
|
|
707
|
-
expect(clearedHandles).toContain(createdHandles[1]);
|
|
708
|
-
} finally {
|
|
709
|
-
globalThis.setInterval = originalSetInterval;
|
|
710
|
-
globalThis.clearInterval = originalClearInterval;
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
test("session_before_switch clears pending compact", async () => {
|
|
715
|
-
let compactCalled = false;
|
|
716
|
-
const toolCtx = buildContext({ compact: () => {} });
|
|
717
|
-
const agentEndCtx = buildContext({
|
|
718
|
-
compact: () => {
|
|
719
|
-
compactCalled = true;
|
|
720
|
-
},
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
// Set pending compact
|
|
724
|
-
await executeTool({ command: "compact" }, toolCtx);
|
|
725
|
-
|
|
726
|
-
// Session switch fires — should clear the flag
|
|
727
|
-
await harness.fireEvent("session_before_switch", {
|
|
728
|
-
type: "session_before_switch",
|
|
729
|
-
reason: "switch",
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
// agent_end should now be a no-op
|
|
733
|
-
await harness.fireEvent("agent_end", { type: "agent_end", messages: [] }, agentEndCtx);
|
|
734
|
-
|
|
735
|
-
expect(compactCalled).toBe(false);
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// ── Error handling ───────────────────────────────────────────────────────────
|
|
740
|
-
|
|
741
|
-
describe("error handling", () => {
|
|
742
|
-
test("rejects unknown commands", async () => {
|
|
743
|
-
const ctx = buildContext();
|
|
744
|
-
|
|
745
|
-
const result = await executeTool({ command: "nonexistent" }, ctx);
|
|
746
|
-
|
|
747
|
-
expect(result.isError).toBe(true);
|
|
748
|
-
const text = result.content[0];
|
|
749
|
-
if (text?.type === "text") {
|
|
750
|
-
expect(text.text).toContain("Unknown command");
|
|
751
|
-
expect(text.text).toContain("nonexistent");
|
|
752
|
-
expect(text.text).toContain("show-system-prompt");
|
|
753
|
-
expect(text.text).toContain("context");
|
|
754
|
-
expect(text.text).toContain("compact");
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
test("rejects commands with / prefix", async () => {
|
|
759
|
-
const ctx = buildContext();
|
|
760
|
-
|
|
761
|
-
const result = await executeTool({ command: "/compact" }, ctx);
|
|
762
|
-
|
|
763
|
-
expect(result.isError).toBe(true);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
test("rejects empty command string", async () => {
|
|
767
|
-
const ctx = buildContext();
|
|
768
|
-
|
|
769
|
-
const result = await executeTool({ command: "" }, ctx);
|
|
770
|
-
|
|
771
|
-
expect(result.isError).toBe(true);
|
|
772
|
-
});
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
// ── Context injection ────────────────────────────────────────────────────────
|
|
776
|
-
|
|
777
|
-
describe("context injection", () => {
|
|
778
|
-
test("injects hidden message listing bridged commands", async () => {
|
|
779
|
-
const results = await harness.fireEvent("before_agent_start", {
|
|
780
|
-
type: "before_agent_start",
|
|
781
|
-
prompt: "hello",
|
|
782
|
-
systemPrompt: "",
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
const result = results.find((r) => r != null) as
|
|
786
|
-
| {
|
|
787
|
-
message: { customType: string; content: string; display: boolean };
|
|
788
|
-
}
|
|
789
|
-
| undefined;
|
|
790
|
-
|
|
791
|
-
expect(result).toBeDefined();
|
|
792
|
-
expect(result?.message.customType).toBe("slash-command-bridge-context");
|
|
793
|
-
expect(result?.message.display).toBe(false);
|
|
794
|
-
expect(result?.message.content).toContain("run_slash_command");
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
test("context message mentions available commands", async () => {
|
|
798
|
-
const results = await harness.fireEvent("before_agent_start", {
|
|
799
|
-
type: "before_agent_start",
|
|
800
|
-
prompt: "hello",
|
|
801
|
-
systemPrompt: "",
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
const result = results.find((r) => r != null) as
|
|
805
|
-
| {
|
|
806
|
-
message: { content: string };
|
|
807
|
-
}
|
|
808
|
-
| undefined;
|
|
809
|
-
|
|
810
|
-
expect(result?.message.content).toContain("/show-system-prompt");
|
|
811
|
-
expect(result?.message.content).toContain("/context");
|
|
812
|
-
expect(result?.message.content).toContain("/compact");
|
|
457
|
+
expect(harness.sentMessages).toHaveLength(0);
|
|
458
|
+
expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
|
|
459
|
+
expect(workingMessages.at(-1)).toBeUndefined();
|
|
813
460
|
});
|
|
814
461
|
});
|
|
815
|
-
|
|
816
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Build a mock ExtensionContext with overridable methods.
|
|
820
|
-
*
|
|
821
|
-
* @param overrides - Methods to override on the default stub context
|
|
822
|
-
* @returns Mock ExtensionContext
|
|
823
|
-
*/
|
|
824
|
-
function buildContext(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
825
|
-
return {
|
|
826
|
-
ui: {} as ExtensionContext["ui"],
|
|
827
|
-
hasUI: false,
|
|
828
|
-
cwd: process.cwd(),
|
|
829
|
-
sessionManager: {} as ExtensionContext["sessionManager"],
|
|
830
|
-
modelRegistry: {} as ExtensionContext["modelRegistry"],
|
|
831
|
-
model: undefined,
|
|
832
|
-
isIdle: () => true,
|
|
833
|
-
abort: () => {},
|
|
834
|
-
hasPendingMessages: () => false,
|
|
835
|
-
shutdown: () => {},
|
|
836
|
-
getContextUsage: () => undefined,
|
|
837
|
-
compact: () => {},
|
|
838
|
-
getSystemPrompt: () => "",
|
|
839
|
-
...overrides,
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Waits for a given number of milliseconds.
|
|
845
|
-
*
|
|
846
|
-
* @param milliseconds - Delay duration in milliseconds
|
|
847
|
-
* @returns Promise that resolves after the delay
|
|
848
|
-
*/
|
|
849
|
-
function sleep(milliseconds: number): Promise<void> {
|
|
850
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Execute the run_slash_command tool with the given params and context.
|
|
855
|
-
*
|
|
856
|
-
* @param params - Tool parameters
|
|
857
|
-
* @param ctx - Extension context (optional, uses default stub)
|
|
858
|
-
* @returns Tool execution result
|
|
859
|
-
*/
|
|
860
|
-
async function executeTool(params: { command: string }, ctx?: ExtensionContext) {
|
|
861
|
-
const tool = harness.tools.get("run_slash_command");
|
|
862
|
-
if (!tool) throw new Error("run_slash_command tool not registered");
|
|
863
|
-
|
|
864
|
-
return tool.execute("test-call-id", params, undefined, undefined, ctx ?? buildContext());
|
|
865
|
-
}
|