@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url, {
|
|
6
|
+
interopDefault: true,
|
|
7
|
+
moduleCache: false,
|
|
8
|
+
});
|
|
9
|
+
const { AgentSessionWrapper } = await jiti.import("./rpc-manager.ts");
|
|
10
|
+
|
|
11
|
+
const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
12
|
+
|
|
13
|
+
function makePromptInner(prompt) {
|
|
14
|
+
return {
|
|
15
|
+
isBashRunning: false,
|
|
16
|
+
isStreaming: false,
|
|
17
|
+
extensionRunner: {},
|
|
18
|
+
sessionManager: { getCwd: () => "/tmp" },
|
|
19
|
+
agent: { state: {} },
|
|
20
|
+
getContextUsage: () => null,
|
|
21
|
+
getSteeringMessages: () => [],
|
|
22
|
+
getFollowUpMessages: () => [],
|
|
23
|
+
prompt,
|
|
24
|
+
dispose() {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("get_state waits for extension resources before returning the system prompt", async (t) => {
|
|
29
|
+
let finishBinding;
|
|
30
|
+
const inner = makePromptInner(() => Promise.resolve());
|
|
31
|
+
inner.agent.state.systemPrompt = "before extensions";
|
|
32
|
+
inner.bindExtensions = () => new Promise((resolve) => {
|
|
33
|
+
finishBinding = () => {
|
|
34
|
+
inner.agent.state.systemPrompt = "after extensions";
|
|
35
|
+
resolve();
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
40
|
+
t.after(() => wrapper.destroy());
|
|
41
|
+
wrapper.beginExtensionBinding();
|
|
42
|
+
|
|
43
|
+
let settled = false;
|
|
44
|
+
const statePromise = wrapper.send({ type: "get_state" }).then((state) => {
|
|
45
|
+
settled = true;
|
|
46
|
+
return state;
|
|
47
|
+
});
|
|
48
|
+
await nextTurn();
|
|
49
|
+
|
|
50
|
+
assert.equal(settled, false);
|
|
51
|
+
finishBinding();
|
|
52
|
+
const state = await statePromise;
|
|
53
|
+
|
|
54
|
+
assert.equal(state.systemPrompt, "after extensions");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("prompt commands wait for SDK preflight acceptance before acknowledging", async (t) => {
|
|
58
|
+
let acceptPreflight;
|
|
59
|
+
let finishPrompt;
|
|
60
|
+
const inner = makePromptInner((_message, options) => new Promise((resolve) => {
|
|
61
|
+
acceptPreflight = () => options.preflightResult(true);
|
|
62
|
+
finishPrompt = resolve;
|
|
63
|
+
}));
|
|
64
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
65
|
+
t.after(() => wrapper.destroy());
|
|
66
|
+
const events = [];
|
|
67
|
+
wrapper.onEvent((event) => events.push(event));
|
|
68
|
+
|
|
69
|
+
let acknowledged = false;
|
|
70
|
+
const sending = wrapper.send({ type: "prompt", message: "hello" }).then(() => {
|
|
71
|
+
acknowledged = true;
|
|
72
|
+
});
|
|
73
|
+
await nextTurn();
|
|
74
|
+
|
|
75
|
+
assert.equal(acknowledged, false);
|
|
76
|
+
assert.equal(wrapper.isRunning(), true);
|
|
77
|
+
|
|
78
|
+
acceptPreflight();
|
|
79
|
+
await sending;
|
|
80
|
+
|
|
81
|
+
assert.equal(acknowledged, true);
|
|
82
|
+
assert.equal(wrapper.isRunning(), true);
|
|
83
|
+
assert.equal(events.some((event) => event.type === "prompt_done"), false);
|
|
84
|
+
|
|
85
|
+
finishPrompt();
|
|
86
|
+
await nextTurn();
|
|
87
|
+
|
|
88
|
+
assert.equal(wrapper.isRunning(), false);
|
|
89
|
+
assert.equal(events.filter((event) => event.type === "prompt_done").length, 1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("prompt commands reject when SDK preflight fails", async (t) => {
|
|
93
|
+
const inner = makePromptInner((_message, options) => {
|
|
94
|
+
options.preflightResult(false);
|
|
95
|
+
return Promise.reject(new Error("Authentication failed"));
|
|
96
|
+
});
|
|
97
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
98
|
+
t.after(() => wrapper.destroy());
|
|
99
|
+
const events = [];
|
|
100
|
+
wrapper.onEvent((event) => events.push(event));
|
|
101
|
+
|
|
102
|
+
await assert.rejects(
|
|
103
|
+
wrapper.send({ type: "prompt", message: "hello" }),
|
|
104
|
+
/Authentication failed/,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
assert.equal(wrapper.isRunning(), false);
|
|
108
|
+
assert.deepEqual(events, []);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("accepted prompt failures still finish through the event stream", async (t) => {
|
|
112
|
+
let failPrompt;
|
|
113
|
+
const inner = makePromptInner((_message, options) => {
|
|
114
|
+
options.preflightResult(true);
|
|
115
|
+
return new Promise((_resolve, reject) => {
|
|
116
|
+
failPrompt = () => reject(new Error("post-accept failure"));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
120
|
+
t.after(() => wrapper.destroy());
|
|
121
|
+
const events = [];
|
|
122
|
+
wrapper.onEvent((event) => events.push(event));
|
|
123
|
+
|
|
124
|
+
await wrapper.send({ type: "prompt", message: "hello" });
|
|
125
|
+
failPrompt();
|
|
126
|
+
await nextTurn();
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(events.map((event) => event.type), ["prompt_error", "prompt_done"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("queued prompt commands forward their streaming behavior and acknowledge acceptance", async (t) => {
|
|
132
|
+
let receivedOptions;
|
|
133
|
+
const inner = makePromptInner((_message, options) => {
|
|
134
|
+
receivedOptions = options;
|
|
135
|
+
options.preflightResult(true);
|
|
136
|
+
return Promise.resolve();
|
|
137
|
+
});
|
|
138
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
139
|
+
t.after(() => wrapper.destroy());
|
|
140
|
+
const events = [];
|
|
141
|
+
wrapper.onEvent((event) => events.push(event));
|
|
142
|
+
|
|
143
|
+
await wrapper.send({
|
|
144
|
+
type: "prompt",
|
|
145
|
+
message: "next",
|
|
146
|
+
streamingBehavior: "followUp",
|
|
147
|
+
});
|
|
148
|
+
await nextTurn();
|
|
149
|
+
|
|
150
|
+
assert.equal(receivedOptions.streamingBehavior, "followUp");
|
|
151
|
+
assert.equal(receivedOptions.source, "rpc");
|
|
152
|
+
assert.equal(wrapper.isRunning(), false);
|
|
153
|
+
assert.deepEqual(events, []);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("prompt admission waits for the preceding preflight and keeps overlapping runs counted", async (t) => {
|
|
157
|
+
let callCount = 0;
|
|
158
|
+
let acceptFirst;
|
|
159
|
+
let finishFirst;
|
|
160
|
+
const inner = makePromptInner((_message, options) => {
|
|
161
|
+
callCount += 1;
|
|
162
|
+
if (callCount === 1) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
acceptFirst = () => {
|
|
165
|
+
inner.isStreaming = true;
|
|
166
|
+
options.preflightResult(true);
|
|
167
|
+
};
|
|
168
|
+
finishFirst = () => {
|
|
169
|
+
inner.isStreaming = false;
|
|
170
|
+
resolve();
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
assert.equal(inner.isStreaming, true);
|
|
176
|
+
options.preflightResult(true);
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
});
|
|
179
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
180
|
+
t.after(() => wrapper.destroy());
|
|
181
|
+
|
|
182
|
+
const first = wrapper.send({ type: "prompt", message: "first" });
|
|
183
|
+
await nextTurn();
|
|
184
|
+
const queued = wrapper.send({
|
|
185
|
+
type: "prompt",
|
|
186
|
+
message: "queued",
|
|
187
|
+
streamingBehavior: "followUp",
|
|
188
|
+
});
|
|
189
|
+
await nextTurn();
|
|
190
|
+
|
|
191
|
+
assert.equal(callCount, 1);
|
|
192
|
+
acceptFirst();
|
|
193
|
+
await Promise.all([first, queued]);
|
|
194
|
+
await nextTurn();
|
|
195
|
+
|
|
196
|
+
assert.equal(callCount, 2);
|
|
197
|
+
assert.equal((await wrapper.send({ type: "get_state" })).isPromptRunning, true);
|
|
198
|
+
|
|
199
|
+
finishFirst();
|
|
200
|
+
await nextTurn();
|
|
201
|
+
|
|
202
|
+
assert.equal((await wrapper.send({ type: "get_state" })).isPromptRunning, false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("prompt admission continues after the preceding preflight rejects", async (t) => {
|
|
206
|
+
let callCount = 0;
|
|
207
|
+
let rejectFirst;
|
|
208
|
+
const inner = makePromptInner((_message, options) => {
|
|
209
|
+
callCount += 1;
|
|
210
|
+
if (callCount === 1) {
|
|
211
|
+
return new Promise((_resolve, reject) => {
|
|
212
|
+
rejectFirst = () => {
|
|
213
|
+
options.preflightResult(false);
|
|
214
|
+
reject(new Error("first rejected"));
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
options.preflightResult(true);
|
|
219
|
+
return Promise.resolve();
|
|
220
|
+
});
|
|
221
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
222
|
+
t.after(() => wrapper.destroy());
|
|
223
|
+
|
|
224
|
+
const firstRejected = assert.rejects(
|
|
225
|
+
wrapper.send({ type: "prompt", message: "first" }),
|
|
226
|
+
/first rejected/,
|
|
227
|
+
);
|
|
228
|
+
await nextTurn();
|
|
229
|
+
const second = wrapper.send({ type: "prompt", message: "second" });
|
|
230
|
+
await nextTurn();
|
|
231
|
+
assert.equal(callCount, 1);
|
|
232
|
+
|
|
233
|
+
rejectFirst();
|
|
234
|
+
await Promise.all([firstRejected, second]);
|
|
235
|
+
|
|
236
|
+
assert.equal(callCount, 2);
|
|
237
|
+
assert.equal(wrapper.isRunning(), false);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a failing event listener cannot reject prompt completion", async (t) => {
|
|
241
|
+
const originalConsoleError = console.error;
|
|
242
|
+
t.after(() => {
|
|
243
|
+
console.error = originalConsoleError;
|
|
244
|
+
});
|
|
245
|
+
console.error = () => {};
|
|
246
|
+
|
|
247
|
+
const inner = makePromptInner((_message, options) => {
|
|
248
|
+
options.preflightResult(true);
|
|
249
|
+
return Promise.resolve();
|
|
250
|
+
});
|
|
251
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
252
|
+
t.after(() => wrapper.destroy());
|
|
253
|
+
const delivered = [];
|
|
254
|
+
wrapper.onEvent(() => {
|
|
255
|
+
throw new Error("listener failed");
|
|
256
|
+
});
|
|
257
|
+
wrapper.onEvent((event) => delivered.push(event.type));
|
|
258
|
+
|
|
259
|
+
await wrapper.send({ type: "prompt", message: "hello" });
|
|
260
|
+
await nextTurn();
|
|
261
|
+
|
|
262
|
+
assert.deepEqual(delivered, ["prompt_done"]);
|
|
263
|
+
assert.equal(wrapper.isRunning(), false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("session shutdown notifies extensions before disposing the SDK session", async () => {
|
|
267
|
+
const calls = [];
|
|
268
|
+
const inner = {
|
|
269
|
+
isBashRunning: false,
|
|
270
|
+
extensionRunner: {
|
|
271
|
+
async emit(event) {
|
|
272
|
+
calls.push(["emit", event]);
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
dispose() {
|
|
276
|
+
calls.push(["dispose"]);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
280
|
+
wrapper.onDestroy(() => calls.push(["destroy"]));
|
|
281
|
+
|
|
282
|
+
await Promise.all([wrapper.shutdown(), wrapper.shutdown()]);
|
|
283
|
+
|
|
284
|
+
assert.deepEqual(calls, [
|
|
285
|
+
["emit", { type: "session_shutdown", reason: "quit" }],
|
|
286
|
+
["dispose"],
|
|
287
|
+
["destroy"],
|
|
288
|
+
]);
|
|
289
|
+
assert.equal(wrapper.isAlive(), false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("session shutdown still disposes the SDK session when an extension fails", async () => {
|
|
293
|
+
const calls = [];
|
|
294
|
+
const inner = {
|
|
295
|
+
isBashRunning: false,
|
|
296
|
+
extensionRunner: {
|
|
297
|
+
async emit() {
|
|
298
|
+
calls.push("emit");
|
|
299
|
+
throw new Error("shutdown hook failed");
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
dispose() {
|
|
303
|
+
calls.push("dispose");
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
307
|
+
wrapper.onDestroy(() => calls.push("destroy"));
|
|
308
|
+
|
|
309
|
+
await assert.rejects(wrapper.shutdown(), /shutdown hook failed/);
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(calls, ["emit", "dispose", "destroy"]);
|
|
312
|
+
assert.equal(wrapper.isAlive(), false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("direct destruction disposes the SDK session before unregistering the wrapper", () => {
|
|
316
|
+
const calls = [];
|
|
317
|
+
const inner = {
|
|
318
|
+
isBashRunning: false,
|
|
319
|
+
extensionRunner: {},
|
|
320
|
+
dispose() {
|
|
321
|
+
calls.push("dispose");
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
325
|
+
wrapper.onDestroy(() => calls.push("destroy"));
|
|
326
|
+
|
|
327
|
+
wrapper.destroy();
|
|
328
|
+
wrapper.destroy();
|
|
329
|
+
|
|
330
|
+
assert.deepEqual(calls, ["dispose", "destroy"]);
|
|
331
|
+
assert.equal(wrapper.isAlive(), false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("direct bash commands use sanitized project operations with current shell settings", async (t) => {
|
|
335
|
+
let received;
|
|
336
|
+
let shellPath = "/bin/bash";
|
|
337
|
+
const inner = {
|
|
338
|
+
isBashRunning: false,
|
|
339
|
+
isStreaming: false,
|
|
340
|
+
isCompacting: false,
|
|
341
|
+
extensionRunner: {},
|
|
342
|
+
settingsManager: {
|
|
343
|
+
getShellPath: () => shellPath,
|
|
344
|
+
},
|
|
345
|
+
sessionManager: {
|
|
346
|
+
getCwd: () => process.cwd(),
|
|
347
|
+
getSessionFile: () => undefined,
|
|
348
|
+
},
|
|
349
|
+
agent: { state: {} },
|
|
350
|
+
getContextUsage: () => null,
|
|
351
|
+
getSteeringMessages: () => [],
|
|
352
|
+
getFollowUpMessages: () => [],
|
|
353
|
+
executeBash: async (command, _onChunk, options) => {
|
|
354
|
+
received = { command, options };
|
|
355
|
+
return { output: "", exitCode: 0 };
|
|
356
|
+
},
|
|
357
|
+
dispose() {},
|
|
358
|
+
};
|
|
359
|
+
const wrapper = new AgentSessionWrapper(inner);
|
|
360
|
+
t.after(() => wrapper.destroy());
|
|
361
|
+
|
|
362
|
+
shellPath = "/custom/bash";
|
|
363
|
+
await wrapper.send({
|
|
364
|
+
type: "bash",
|
|
365
|
+
command: "echo ready",
|
|
366
|
+
excludeFromContext: true,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
assert.equal(received.command, "echo ready");
|
|
370
|
+
assert.equal(received.options.excludeFromContext, true);
|
|
371
|
+
assert.equal(typeof received.options.operations.exec, "function");
|
|
372
|
+
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url, {
|
|
6
|
+
interopDefault: true,
|
|
7
|
+
moduleCache: false,
|
|
8
|
+
});
|
|
9
|
+
const { AgentSessionWrapper } = await jiti.import("./rpc-manager.ts");
|
|
10
|
+
|
|
11
|
+
function makeInner(overrides = {}) {
|
|
12
|
+
return {
|
|
13
|
+
sessionId: "widget-test-session",
|
|
14
|
+
sessionFile: undefined,
|
|
15
|
+
isStreaming: false,
|
|
16
|
+
isCompacting: false,
|
|
17
|
+
isBashRunning: false,
|
|
18
|
+
autoCompactionEnabled: true,
|
|
19
|
+
autoRetryEnabled: true,
|
|
20
|
+
model: undefined,
|
|
21
|
+
modelRuntime: {
|
|
22
|
+
getModel: () => undefined,
|
|
23
|
+
refresh: async () => {},
|
|
24
|
+
},
|
|
25
|
+
sessionManager: { getCwd: () => process.cwd() },
|
|
26
|
+
settingsManager: { setProjectTrusted: () => {} },
|
|
27
|
+
agent: { state: {} },
|
|
28
|
+
extensionRunner: {
|
|
29
|
+
getRegisteredCommands: () => [],
|
|
30
|
+
setUIContext: () => {},
|
|
31
|
+
emit: async () => {},
|
|
32
|
+
},
|
|
33
|
+
promptTemplates: [],
|
|
34
|
+
resourceLoader: { getSkills: () => ({ skills: [] }) },
|
|
35
|
+
subscribe: () => () => {},
|
|
36
|
+
getContextUsage: () => null,
|
|
37
|
+
getSteeringMessages: () => [],
|
|
38
|
+
getFollowUpMessages: () => [],
|
|
39
|
+
pendingMessageCount: 0,
|
|
40
|
+
dispose: () => {},
|
|
41
|
+
reload: async () => {},
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createContext(overrides = {}) {
|
|
47
|
+
const events = [];
|
|
48
|
+
const wrapper = new AgentSessionWrapper(makeInner(overrides));
|
|
49
|
+
wrapper.onEvent((event) => events.push(event));
|
|
50
|
+
const context = wrapper.createExtensionUiContext();
|
|
51
|
+
return { context, events, wrapper };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function widgetEvents(events) {
|
|
55
|
+
return events.filter((event) => event.type === "extension_ui_request" && event.method === "setWidget");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function snapshot(wrapper) {
|
|
59
|
+
return wrapper.send({ type: "get_state" }).then((state) => state.extensionWidgets);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("keeps array widgets compatible while maintaining the state snapshot", async () => {
|
|
63
|
+
const { context, events, wrapper } = createContext();
|
|
64
|
+
|
|
65
|
+
context.setWidget("array", ["one"], { placement: "belowEditor" });
|
|
66
|
+
assert.deepEqual(widgetEvents(events).at(-1), {
|
|
67
|
+
type: "extension_ui_request",
|
|
68
|
+
id: widgetEvents(events).at(-1).id,
|
|
69
|
+
method: "setWidget",
|
|
70
|
+
widgetKey: "array",
|
|
71
|
+
widgetLines: ["one"],
|
|
72
|
+
widgetPlacement: "belowEditor",
|
|
73
|
+
});
|
|
74
|
+
assert.deepEqual(await snapshot(wrapper), [{ key: "array", lines: ["one"], placement: "belowEditor" }]);
|
|
75
|
+
|
|
76
|
+
const eventCount = widgetEvents(events).length;
|
|
77
|
+
context.setWidget("array", ["two"]);
|
|
78
|
+
assert.equal(widgetEvents(events).length, eventCount + 1);
|
|
79
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines[0], "two");
|
|
80
|
+
assert.equal(widgetEvents(events).at(-1).widgetPlacement, undefined);
|
|
81
|
+
assert.deepEqual(await snapshot(wrapper), [{ key: "array", lines: ["two"], placement: "aboveEditor" }]);
|
|
82
|
+
wrapper.destroy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("renders a factory widget immediately with the fixed headless facade", async () => {
|
|
86
|
+
const { context, events, wrapper } = createContext();
|
|
87
|
+
let tui;
|
|
88
|
+
let theme;
|
|
89
|
+
const widths = [];
|
|
90
|
+
|
|
91
|
+
context.setWidget("factory", (receivedTui, receivedTheme) => {
|
|
92
|
+
tui = receivedTui;
|
|
93
|
+
theme = receivedTheme;
|
|
94
|
+
return {
|
|
95
|
+
render(width) {
|
|
96
|
+
widths.push(width);
|
|
97
|
+
return [`${width}:${tui.terminal.columns}`];
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}, { placement: "belowEditor" });
|
|
101
|
+
|
|
102
|
+
const event = widgetEvents(events).at(-1);
|
|
103
|
+
assert.equal(tui.terminal.columns, 92);
|
|
104
|
+
assert.equal(tui.terminal.kittyProtocolActive, false);
|
|
105
|
+
assert.ok(theme);
|
|
106
|
+
assert.deepEqual(widths, [92]);
|
|
107
|
+
assert.deepEqual(event.widgetLines, ["92:92"]);
|
|
108
|
+
assert.equal(event.widgetPlacement, "belowEditor");
|
|
109
|
+
assert.deepEqual(await snapshot(wrapper), [{ key: "factory", lines: ["92:92"], placement: "belowEditor" }]);
|
|
110
|
+
wrapper.destroy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("requestRender replaces the browser event and server snapshot", async () => {
|
|
114
|
+
const { context, events, wrapper } = createContext();
|
|
115
|
+
let tui;
|
|
116
|
+
let value = "first";
|
|
117
|
+
context.setWidget("refreshable", (receivedTui) => {
|
|
118
|
+
tui = receivedTui;
|
|
119
|
+
return { render: () => [value] };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const before = widgetEvents(events).length;
|
|
123
|
+
value = "second";
|
|
124
|
+
tui.requestRender();
|
|
125
|
+
assert.equal(widgetEvents(events).length, before + 1);
|
|
126
|
+
assert.deepEqual(widgetEvents(events).at(-1).widgetLines, ["second"]);
|
|
127
|
+
assert.deepEqual(await snapshot(wrapper), [{ key: "refreshable", lines: ["second"], placement: "aboveEditor" }]);
|
|
128
|
+
wrapper.destroy();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("clearing and replacing widgets disposes components and invalidates old callbacks", () => {
|
|
132
|
+
const { context, events, wrapper } = createContext();
|
|
133
|
+
let oldTui;
|
|
134
|
+
let oldDisposed = 0;
|
|
135
|
+
context.setWidget("shared", (tui) => {
|
|
136
|
+
oldTui = tui;
|
|
137
|
+
return { render: () => ["old"], dispose: () => { oldDisposed += 1; } };
|
|
138
|
+
});
|
|
139
|
+
const beforeReplace = widgetEvents(events).length;
|
|
140
|
+
|
|
141
|
+
context.setWidget("shared", ["array"]);
|
|
142
|
+
assert.equal(oldDisposed, 1);
|
|
143
|
+
assert.deepEqual(widgetEvents(events).at(-1).widgetLines, ["array"]);
|
|
144
|
+
const afterReplace = widgetEvents(events).length;
|
|
145
|
+
oldTui.requestRender();
|
|
146
|
+
assert.equal(widgetEvents(events).length, afterReplace);
|
|
147
|
+
|
|
148
|
+
let newTui;
|
|
149
|
+
let newDisposed = 0;
|
|
150
|
+
context.setWidget("shared", (tui) => {
|
|
151
|
+
newTui = tui;
|
|
152
|
+
return { render: () => ["new"], dispose: () => { newDisposed += 1; } };
|
|
153
|
+
});
|
|
154
|
+
const afterFactoryReplace = widgetEvents(events).length;
|
|
155
|
+
oldTui.requestRender();
|
|
156
|
+
assert.equal(widgetEvents(events).length, afterFactoryReplace);
|
|
157
|
+
|
|
158
|
+
context.setWidget("shared", undefined);
|
|
159
|
+
assert.equal(newDisposed, 1);
|
|
160
|
+
const afterClear = widgetEvents(events).length;
|
|
161
|
+
newTui.requestRender();
|
|
162
|
+
assert.equal(widgetEvents(events).length, afterClear);
|
|
163
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
164
|
+
assert.ok(beforeReplace < afterReplace);
|
|
165
|
+
wrapper.destroy();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("isolates factory and render failures with a clear and extension_error", async () => {
|
|
169
|
+
const { context, events, wrapper } = createContext();
|
|
170
|
+
context.setWidget("factory-error", () => {
|
|
171
|
+
throw new Error("factory failed");
|
|
172
|
+
});
|
|
173
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
174
|
+
assert.equal(events.at(-1).type, "extension_error");
|
|
175
|
+
assert.match(events.at(-1).error, /factory failed/);
|
|
176
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
177
|
+
|
|
178
|
+
context.setWidget("factory-error", () => ({ render: () => ["recovered"] }));
|
|
179
|
+
assert.deepEqual(await snapshot(wrapper), [{ key: "factory-error", lines: ["recovered"], placement: "aboveEditor" }]);
|
|
180
|
+
context.setWidget("factory-error", undefined);
|
|
181
|
+
|
|
182
|
+
context.setWidget("invalid-component", () => ({
|
|
183
|
+
dispose: () => { throw new Error("dispose failed"); },
|
|
184
|
+
}));
|
|
185
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
186
|
+
assert.match(events.at(-1).error, /render\(width\)/);
|
|
187
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
188
|
+
|
|
189
|
+
let disposed = 0;
|
|
190
|
+
context.setWidget("render-error", () => ({
|
|
191
|
+
render: () => { throw new Error("render failed"); },
|
|
192
|
+
dispose: () => { disposed += 1; },
|
|
193
|
+
}));
|
|
194
|
+
assert.equal(disposed, 1);
|
|
195
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
196
|
+
assert.match(events.at(-1).error, /render failed/);
|
|
197
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
198
|
+
|
|
199
|
+
context.setWidget("invalid-lines", () => ({ render: () => ["ok", 7] }));
|
|
200
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
201
|
+
assert.match(events.at(-1).error, /string\[\]/);
|
|
202
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
203
|
+
|
|
204
|
+
let tui;
|
|
205
|
+
let shouldThrow = false;
|
|
206
|
+
context.setWidget("late-render", (receivedTui) => {
|
|
207
|
+
tui = receivedTui;
|
|
208
|
+
return {
|
|
209
|
+
render: () => {
|
|
210
|
+
if (shouldThrow) throw new Error("late render failed");
|
|
211
|
+
return ["live"];
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
shouldThrow = true;
|
|
216
|
+
tui.requestRender();
|
|
217
|
+
assert.equal(widgetEvents(events).at(-1).widgetLines, undefined);
|
|
218
|
+
assert.match(events.at(-1).error, /late render failed/);
|
|
219
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
220
|
+
wrapper.destroy();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("reload and destroy dispose factory components and invalidate callbacks", async () => {
|
|
224
|
+
const reloads = [];
|
|
225
|
+
const { context, events, wrapper } = createContext({
|
|
226
|
+
reload: async (options) => {
|
|
227
|
+
reloads.push(options);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
let tui;
|
|
231
|
+
let disposed = 0;
|
|
232
|
+
context.setWidget("lifecycle", (receivedTui) => {
|
|
233
|
+
tui = receivedTui;
|
|
234
|
+
return { render: () => ["live"], dispose: () => { disposed += 1; } };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await wrapper.send({ type: "reload" });
|
|
238
|
+
assert.equal(disposed, 1);
|
|
239
|
+
assert.equal(reloads.length, 1);
|
|
240
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
241
|
+
const afterReload = widgetEvents(events).length;
|
|
242
|
+
tui.requestRender();
|
|
243
|
+
assert.equal(widgetEvents(events).length, afterReload);
|
|
244
|
+
|
|
245
|
+
let replacementTui;
|
|
246
|
+
context.setWidget("lifecycle", (receivedTui) => {
|
|
247
|
+
replacementTui = receivedTui;
|
|
248
|
+
return { render: () => ["replacement"], dispose: () => { disposed += 1; } };
|
|
249
|
+
});
|
|
250
|
+
wrapper.destroy();
|
|
251
|
+
assert.equal(disposed, 2);
|
|
252
|
+
const afterDestroy = widgetEvents(events).length;
|
|
253
|
+
replacementTui.requestRender();
|
|
254
|
+
assert.equal(widgetEvents(events).length, afterDestroy);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("command-context reload also clears factory widgets", async () => {
|
|
258
|
+
const { context, wrapper } = createContext();
|
|
259
|
+
let disposed = 0;
|
|
260
|
+
context.setWidget("command-reload", () => ({
|
|
261
|
+
render: () => ["live"],
|
|
262
|
+
dispose: () => { disposed += 1; },
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
await wrapper.createExtensionCommandContextActions().reload();
|
|
266
|
+
assert.equal(disposed, 1);
|
|
267
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
268
|
+
wrapper.destroy();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("dispose-time reentrant updates remain authoritative", async () => {
|
|
272
|
+
const { context, events, wrapper } = createContext();
|
|
273
|
+
let replacementTui;
|
|
274
|
+
|
|
275
|
+
context.setWidget("reentrant", () => ({
|
|
276
|
+
render: () => ["old"],
|
|
277
|
+
dispose: () => {
|
|
278
|
+
context.setWidget("reentrant", (tui) => {
|
|
279
|
+
replacementTui = tui;
|
|
280
|
+
return { render: () => ["replacement"] };
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
context.setWidget("reentrant", ["outer"]);
|
|
286
|
+
assert.deepEqual(await snapshot(wrapper), [{
|
|
287
|
+
key: "reentrant",
|
|
288
|
+
lines: ["replacement"],
|
|
289
|
+
placement: "aboveEditor",
|
|
290
|
+
}]);
|
|
291
|
+
|
|
292
|
+
const beforeRefresh = widgetEvents(events).length;
|
|
293
|
+
replacementTui.requestRender();
|
|
294
|
+
assert.equal(widgetEvents(events).length, beforeRefresh + 1);
|
|
295
|
+
assert.deepEqual(widgetEvents(events).at(-1).widgetLines, ["replacement"]);
|
|
296
|
+
context.setWidget("reentrant", undefined);
|
|
297
|
+
wrapper.destroy();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("render failure does not clear a dispose-time recovery widget", async () => {
|
|
301
|
+
const { context, wrapper } = createContext();
|
|
302
|
+
|
|
303
|
+
context.setWidget("render-recovery", () => ({
|
|
304
|
+
render: () => { throw new Error("render failed"); },
|
|
305
|
+
dispose: () => context.setWidget("render-recovery", ["recovered"]),
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
assert.deepEqual(await snapshot(wrapper), [{
|
|
309
|
+
key: "render-recovery",
|
|
310
|
+
lines: ["recovered"],
|
|
311
|
+
placement: "aboveEditor",
|
|
312
|
+
}]);
|
|
313
|
+
wrapper.destroy();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("ignores factory registrations after the wrapper is destroyed", () => {
|
|
317
|
+
const { context, events, wrapper } = createContext();
|
|
318
|
+
let factoryCalls = 0;
|
|
319
|
+
|
|
320
|
+
wrapper.destroy();
|
|
321
|
+
const before = events.length;
|
|
322
|
+
context.setWidget("late", () => {
|
|
323
|
+
factoryCalls += 1;
|
|
324
|
+
return { render: () => ["late"] };
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
assert.equal(factoryCalls, 0);
|
|
328
|
+
assert.equal(events.length, before);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("reload ignores widget registrations triggered by component disposal", async () => {
|
|
332
|
+
const { context, wrapper } = createContext();
|
|
333
|
+
|
|
334
|
+
context.setWidget("reload-source", () => ({
|
|
335
|
+
render: () => ["live"],
|
|
336
|
+
dispose: () => context.setWidget("reload-stale", () => ({ render: () => ["stale"] })),
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
await wrapper.send({ type: "reload" });
|
|
340
|
+
assert.deepEqual(await snapshot(wrapper), []);
|
|
341
|
+
wrapper.destroy();
|
|
342
|
+
});
|