@meiyukichan/dsh-sandore 0.1.0-rc.6
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/LICENSE +201 -0
- package/cordis.patch.yml +39 -0
- package/dsh.plugin.json +16 -0
- package/lib/client/CommandEditor.d.ts +19 -0
- package/lib/client/DagEditor.d.ts +22 -0
- package/lib/client/DefinitionDetail.d.ts +13 -0
- package/lib/client/DefinitionList.d.ts +19 -0
- package/lib/client/DiscussionInput.d.ts +63 -0
- package/lib/client/DragHandle.d.ts +16 -0
- package/lib/client/EditorPane.d.ts +13 -0
- package/lib/client/FlowEditor.d.ts +31 -0
- package/lib/client/InstanceList.d.ts +24 -0
- package/lib/client/NewInstanceDialog.d.ts +19 -0
- package/lib/client/PreviewPanel.d.ts +13 -0
- package/lib/client/RunView.d.ts +79 -0
- package/lib/client/StepChatPanel.d.ts +34 -0
- package/lib/client/TreeNav.d.ts +27 -0
- package/lib/client/UnifiedTimeline.d.ts +11 -0
- package/lib/client/WorkflowButton.d.ts +11 -0
- package/lib/client/WorkflowList.d.ts +18 -0
- package/lib/client/WorkflowOverlay.d.ts +6 -0
- package/lib/client/chat/AssistantMarkdown.d.ts +22 -0
- package/lib/client/chat/CompactionItem.d.ts +24 -0
- package/lib/client/chat/ContextBody.d.ts +136 -0
- package/lib/client/chat/ContextInjectionRow.d.ts +25 -0
- package/lib/client/chat/GenericCommandCard.d.ts +10 -0
- package/lib/client/chat/MessageIconActions.d.ts +41 -0
- package/lib/client/chat/MessageItem.d.ts +65 -0
- package/lib/client/chat/ReasoningRow.d.ts +13 -0
- package/lib/client/chat/TurnTailNodeView.d.ts +29 -0
- package/lib/client/chat/UserMessageRow.d.ts +18 -0
- package/lib/client/chat/attachment/ImageLightbox.d.ts +26 -0
- package/lib/client/chat/attachment/MessageImage.d.ts +56 -0
- package/lib/client/chat/attachment/image-attachment.d.ts +41 -0
- package/lib/client/chat/attachment/image-labels.d.ts +21 -0
- package/lib/client/chat/message-chrome.d.ts +51 -0
- package/lib/client/chat/reference/ReferenceIcon.d.ts +20 -0
- package/lib/client/chat/turn-assistant.d.ts +12 -0
- package/lib/client/chat/turn-metrics.d.ts +42 -0
- package/lib/client/chat/use-calendar-day.d.ts +5 -0
- package/lib/client/chat/use-throttled-visual-update.d.ts +7 -0
- package/lib/client/chat-flow.d.ts +110 -0
- package/lib/client/composer-selects.d.ts +17 -0
- package/lib/client/dag-editor-model.d.ts +141 -0
- package/lib/client/describe-error.d.ts +9 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/locales.d.ts +777 -0
- package/lib/client/model-faces.d.ts +156 -0
- package/lib/client/open-discussion.d.ts +24 -0
- package/lib/client/panel-model.d.ts +34 -0
- package/lib/client/remote.d.ts +34 -0
- package/lib/client/run-view-model.d.ts +304 -0
- package/lib/client/sandore-save.d.ts +41 -0
- package/lib/client/sandore-sse.d.ts +15 -0
- package/lib/client/step-chat-retry.d.ts +44 -0
- package/lib/client/tool/GenericToolCard.d.ts +28 -0
- package/lib/client/tool/ToolRow.d.ts +95 -0
- package/lib/client/tool/diff-card-model.d.ts +63 -0
- package/lib/client/tool/read-card-model.d.ts +67 -0
- package/lib/client/tool/search-card-model.d.ts +94 -0
- package/lib/client/tool/terminal-card-model.d.ts +86 -0
- package/lib/client/tool/tool-call-model.d.ts +74 -0
- package/lib/client/tool/web-card-model.d.ts +44 -0
- package/lib/client/use-focus-refresh.d.ts +1 -0
- package/lib/client/workflow-face.d.ts +68 -0
- package/lib/client/workflow-store.d.ts +80 -0
- package/lib/client/workspace-path.d.ts +15 -0
- package/lib/client.js +38389 -0
- package/lib/index.js +8854 -0
- package/lib/microflow/branch-evaluator.d.ts +290 -0
- package/lib/microflow/concurrency-limiter.d.ts +39 -0
- package/lib/microflow/errors.d.ts +39 -0
- package/lib/microflow/file-utils.d.ts +210 -0
- package/lib/microflow/finalize-manager.d.ts +414 -0
- package/lib/microflow/fork-session.d.ts +151 -0
- package/lib/microflow/index.d.ts +17 -0
- package/lib/microflow/internal.d.ts +56 -0
- package/lib/microflow/loop-manager.d.ts +127 -0
- package/lib/microflow/rules-resolver.d.ts +28 -0
- package/lib/microflow/stage-executor.d.ts +362 -0
- package/lib/microflow/step-navigator.d.ts +108 -0
- package/lib/microflow/step-runner.d.ts +190 -0
- package/lib/microflow/subagents.d.ts +380 -0
- package/lib/tool/index.d.ts +65 -0
- package/lib/typert.host.d.ts +16 -0
- package/lib/typert.host.js +25 -0
- package/lib/typert.remote-client.d.ts +114 -0
- package/lib/typert.remote-client.js +399 -0
- package/lib/workflow/ask-tool.d.ts +78 -0
- package/lib/workflow/dag-navigator.d.ts +136 -0
- package/lib/workflow/domain.d.ts +304 -0
- package/lib/workflow/engine-surface.d.ts +70 -0
- package/lib/workflow/errors.d.ts +13 -0
- package/lib/workflow/host-agent.d.ts +53 -0
- package/lib/workflow/index.d.ts +23 -0
- package/lib/workflow/interaction-manager.d.ts +170 -0
- package/lib/workflow/lifecycle-manager.d.ts +111 -0
- package/lib/workflow/persistence.d.ts +35 -0
- package/lib/workflow/plugin.d.ts +27 -0
- package/lib/workflow/reactive-state.d.ts +27 -0
- package/lib/workflow/recovery-manager.d.ts +158 -0
- package/lib/workflow/remote.d.ts +139 -0
- package/lib/workflow/schema.d.ts +1917 -0
- package/lib/workflow/seam.d.ts +254 -0
- package/lib/workflow/shell.d.ts +127 -0
- package/lib/workflow/sse.d.ts +16 -0
- package/lib/workflow/typert/typert.host.d.ts +16 -0
- package/lib/workflow/typert/typert.remote-client.d.ts +114 -0
- package/lib/workflow/types.d.ts +187 -0
- package/lib/workflow/wire.d.ts +256 -0
- package/lib/workflow/workflow-engine.d.ts +373 -0
- package/lib/workflow/yaml-error.d.ts +22 -0
- package/lib/workflow/yaml-parser.d.ts +23 -0
- package/package.json +140 -0
- package/packages/client/CommandEditor.tsx +73 -0
- package/packages/client/DagEditor.module.css +711 -0
- package/packages/client/DagEditor.tsx +1203 -0
- package/packages/client/DefinitionDetail.module.css +110 -0
- package/packages/client/DefinitionDetail.tsx +126 -0
- package/packages/client/DefinitionList.tsx +173 -0
- package/packages/client/DiscussionInput.tsx +763 -0
- package/packages/client/DragHandle.tsx +92 -0
- package/packages/client/EditorPane.tsx +70 -0
- package/packages/client/FlowEditor.tsx +227 -0
- package/packages/client/InstanceList.tsx +250 -0
- package/packages/client/NewInstanceDialog.module.css +244 -0
- package/packages/client/NewInstanceDialog.tsx +478 -0
- package/packages/client/PreviewPanel.tsx +55 -0
- package/packages/client/RunView.module.css +246 -0
- package/packages/client/RunView.tsx +314 -0
- package/packages/client/StepChatPanel.tsx +486 -0
- package/packages/client/TreeNav.tsx +294 -0
- package/packages/client/UnifiedTimeline.tsx +484 -0
- package/packages/client/WorkflowButton.module.css +72 -0
- package/packages/client/WorkflowButton.tsx +50 -0
- package/packages/client/WorkflowList.tsx +158 -0
- package/packages/client/WorkflowOverlay.module.css +390 -0
- package/packages/client/WorkflowOverlay.tsx +136 -0
- package/packages/client/WorkflowPanel.module.css +247 -0
- package/packages/client/chat/AssistantMarkdown.module.css +38 -0
- package/packages/client/chat/AssistantMarkdown.tsx +120 -0
- package/packages/client/chat/ChatView.module.css +198 -0
- package/packages/client/chat/CompactionItem.tsx +85 -0
- package/packages/client/chat/ContextBody.module.css +162 -0
- package/packages/client/chat/ContextBody.tsx +598 -0
- package/packages/client/chat/ContextInjectionRow.module.css +63 -0
- package/packages/client/chat/ContextInjectionRow.tsx +78 -0
- package/packages/client/chat/GenericCommandCard.module.css +87 -0
- package/packages/client/chat/GenericCommandCard.tsx +76 -0
- package/packages/client/chat/MessageIconActions.module.css +84 -0
- package/packages/client/chat/MessageIconActions.tsx +147 -0
- package/packages/client/chat/MessageItem.module.css +293 -0
- package/packages/client/chat/MessageItem.tsx +384 -0
- package/packages/client/chat/ReasoningRow.module.css +82 -0
- package/packages/client/chat/ReasoningRow.tsx +70 -0
- package/packages/client/chat/TurnTailNodeView.module.css +10 -0
- package/packages/client/chat/TurnTailNodeView.tsx +63 -0
- package/packages/client/chat/UserMessageRow.module.css +75 -0
- package/packages/client/chat/UserMessageRow.tsx +92 -0
- package/packages/client/chat/accessibility.module.css +9 -0
- package/packages/client/chat/attachment/ImageLightbox.module.css +45 -0
- package/packages/client/chat/attachment/ImageLightbox.tsx +70 -0
- package/packages/client/chat/attachment/MessageImage.module.css +70 -0
- package/packages/client/chat/attachment/MessageImage.tsx +136 -0
- package/packages/client/chat/attachment/image-attachment.ts +98 -0
- package/packages/client/chat/attachment/image-labels.ts +34 -0
- package/packages/client/chat/message-chrome.ts +96 -0
- package/packages/client/chat/reference/ReferenceIcon.tsx +40 -0
- package/packages/client/chat/turn-assistant.ts +15 -0
- package/packages/client/chat/turn-metrics.ts +102 -0
- package/packages/client/chat/use-calendar-day.ts +27 -0
- package/packages/client/chat/use-throttled-visual-update.ts +44 -0
- package/packages/client/chat-flow.spec.ts +357 -0
- package/packages/client/chat-flow.ts +278 -0
- package/packages/client/composer-selects.module.css +376 -0
- package/packages/client/composer-selects.tsx +531 -0
- package/packages/client/dag-editor-model.spec.ts +173 -0
- package/packages/client/dag-editor-model.ts +359 -0
- package/packages/client/describe-error.spec.ts +18 -0
- package/packages/client/describe-error.ts +12 -0
- package/packages/client/index.ts +144 -0
- package/packages/client/input.module.css +854 -0
- package/packages/client/is-conditional-check.spec.ts +34 -0
- package/packages/client/locales.ts +789 -0
- package/packages/client/model-faces.ts +225 -0
- package/packages/client/open-discussion.spec.ts +39 -0
- package/packages/client/open-discussion.ts +32 -0
- package/packages/client/panel-model.spec.ts +152 -0
- package/packages/client/panel-model.ts +72 -0
- package/packages/client/preview.module.css +95 -0
- package/packages/client/react-dom.d.ts +11 -0
- package/packages/client/reactflow-theme.css +91 -0
- package/packages/client/remote.spec.ts +41 -0
- package/packages/client/remote.ts +46 -0
- package/packages/client/run-view-model.spec.ts +641 -0
- package/packages/client/run-view-model.ts +636 -0
- package/packages/client/sandore-save.spec.ts +146 -0
- package/packages/client/sandore-save.ts +97 -0
- package/packages/client/sandore-sse.ts +59 -0
- package/packages/client/step-chat-retry.spec.ts +70 -0
- package/packages/client/step-chat-retry.ts +65 -0
- package/packages/client/timeline.module.css +503 -0
- package/packages/client/tool/GenericToolCard.tsx +99 -0
- package/packages/client/tool/ToolRow.module.css +308 -0
- package/packages/client/tool/ToolRow.tsx +328 -0
- package/packages/client/tool/diff-card-model.ts +103 -0
- package/packages/client/tool/read-card-model.ts +86 -0
- package/packages/client/tool/search-card-model.ts +166 -0
- package/packages/client/tool/terminal-card-model.ts +225 -0
- package/packages/client/tool/tool-call-model.ts +267 -0
- package/packages/client/tool/web-card-model.ts +80 -0
- package/packages/client/tree-nav.module.css +282 -0
- package/packages/client/use-focus-refresh.ts +23 -0
- package/packages/client/workflow-face.ts +72 -0
- package/packages/client/workflow-store.spec.ts +49 -0
- package/packages/client/workflow-store.ts +161 -0
- package/packages/client/workspace-path.ts +27 -0
- package/packages/css-modules.d.ts +6 -0
- package/packages/microflow/branch-evaluator.spec.ts +745 -0
- package/packages/microflow/branch-evaluator.ts +1037 -0
- package/packages/microflow/concurrency-limiter.ts +70 -0
- package/packages/microflow/errors.ts +64 -0
- package/packages/microflow/file-utils.spec.ts +604 -0
- package/packages/microflow/file-utils.ts +521 -0
- package/packages/microflow/finalize-manager.spec.ts +1977 -0
- package/packages/microflow/finalize-manager.ts +1464 -0
- package/packages/microflow/fork-session.spec.ts +184 -0
- package/packages/microflow/fork-session.ts +316 -0
- package/packages/microflow/index.ts +17 -0
- package/packages/microflow/internal.ts +166 -0
- package/packages/microflow/loop-manager.spec.ts +294 -0
- package/packages/microflow/loop-manager.ts +255 -0
- package/packages/microflow/rules-resolver.spec.ts +147 -0
- package/packages/microflow/rules-resolver.ts +58 -0
- package/packages/microflow/stage-executor.spec.ts +1577 -0
- package/packages/microflow/stage-executor.ts +1478 -0
- package/packages/microflow/step-navigator.spec.ts +301 -0
- package/packages/microflow/step-navigator.ts +300 -0
- package/packages/microflow/step-runner.spec.ts +1132 -0
- package/packages/microflow/step-runner.ts +752 -0
- package/packages/microflow/subagents.spec.ts +215 -0
- package/packages/microflow/subagents.ts +422 -0
- package/packages/tests/fixtures/test-workflow.yaml +128 -0
- package/packages/tests/mocks/agents.ts +206 -0
- package/packages/tests/mocks/llm.ts +46 -0
- package/packages/tests/mocks/session-title.ts +31 -0
- package/packages/tests/mocks/sessions.ts +131 -0
- package/packages/tests/mocks/shell.ts +60 -0
- package/packages/tests/mocks/storage-domain.ts +101 -0
- package/packages/tests/mocks/subagents.ts +197 -0
- package/packages/tests/mocks/subprocess.ts +59 -0
- package/packages/tests/mocks/user-questions.ts +33 -0
- package/packages/tests/mocks/workspace.ts +52 -0
- package/packages/tool/index.spec.ts +287 -0
- package/packages/tool/index.ts +348 -0
- package/packages/workflow/ask-tool.ts +173 -0
- package/packages/workflow/dag-navigator.spec.ts +236 -0
- package/packages/workflow/dag-navigator.ts +425 -0
- package/packages/workflow/domain.ts +26 -0
- package/packages/workflow/engine-surface.ts +71 -0
- package/packages/workflow/errors.ts +13 -0
- package/packages/workflow/host-agent.spec.ts +92 -0
- package/packages/workflow/host-agent.ts +62 -0
- package/packages/workflow/index.ts +25 -0
- package/packages/workflow/interaction-manager.spec.ts +390 -0
- package/packages/workflow/interaction-manager.ts +397 -0
- package/packages/workflow/lifecycle-manager.spec.ts +683 -0
- package/packages/workflow/lifecycle-manager.ts +288 -0
- package/packages/workflow/persistence.spec.ts +109 -0
- package/packages/workflow/persistence.ts +64 -0
- package/packages/workflow/plugin.ts +39 -0
- package/packages/workflow/reactive-state.spec.ts +69 -0
- package/packages/workflow/reactive-state.ts +52 -0
- package/packages/workflow/recovery-manager.spec.ts +606 -0
- package/packages/workflow/recovery-manager.ts +471 -0
- package/packages/workflow/remote.spec.ts +1045 -0
- package/packages/workflow/remote.ts +479 -0
- package/packages/workflow/schema.spec.ts +303 -0
- package/packages/workflow/schema.ts +562 -0
- package/packages/workflow/seam.ts +293 -0
- package/packages/workflow/shell.ts +142 -0
- package/packages/workflow/sse.ts +110 -0
- package/packages/workflow/typert/typert.host.d.ts +16 -0
- package/packages/workflow/typert/typert.host.js +25 -0
- package/packages/workflow/typert/typert.remote-client.d.ts +114 -0
- package/packages/workflow/typert/typert.remote-client.js +399 -0
- package/packages/workflow/types.ts +258 -0
- package/packages/workflow/wire.ts +267 -0
- package/packages/workflow/workflow-engine-e2e.spec.ts +262 -0
- package/packages/workflow/workflow-engine-real-e2e.spec.ts +922 -0
- package/packages/workflow/workflow-engine.spec.ts +4389 -0
- package/packages/workflow/workflow-engine.ts +2133 -0
- package/packages/workflow/yaml-error.ts +29 -0
- package/packages/workflow/yaml-parser.spec.ts +757 -0
- package/packages/workflow/yaml-parser.ts +287 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主主代理的 setup 工厂(§3.0:宿主主代理 = 纯 DSH 默认主代理)。
|
|
3
|
+
*
|
|
4
|
+
* `createHostAgentSetup` 生成「同参同闭包」的 `AgentSetup` 回调,供宿主主代理的
|
|
5
|
+
* `ctx.agents.create` 与 `ctx.agents.resume` 共用:回调内唯一动作是挂默认 agent
|
|
6
|
+
* preset(系统提示词/skills/工具全进来),与工作区普通会话走同一组装路径。
|
|
7
|
+
*
|
|
8
|
+
* 实现对齐 apiproxy `composeAgent`(`packages/host/apiproxy/src/api-proxy.ts:1196`):
|
|
9
|
+
* `agentPresets` 服务从引擎 ctx `get` 后**闭包捕获**传入工厂(不在 setup 内直接访问
|
|
10
|
+
* `agentCtx.agentPresets` 属性——agent scope Context 未声明该注入,属性访问会抛
|
|
11
|
+
* `cannot get property "agentPresets" without inject`);setup 内 `presets.mount(agentCtx)`
|
|
12
|
+
* (无 id = 默认 preset)把 agent scope key 挂到默认 preset 的 standing mount。
|
|
13
|
+
*
|
|
14
|
+
* 不叠加任何 sandore 工具、不注入角色提示词(2026-08 探索定夺):
|
|
15
|
+
* - 引擎驱动执行、UI 走 RPC,8 个 `sandore_*` 编排工具没有"必需"消费场景(暂停/
|
|
16
|
+
* 恢复/重试/状态都有按钮);工具 schema 反而占模型上下文、暗示"你是编排器"。
|
|
17
|
+
* - 角色提示词与 DSH 默认系统提示词并存会造成角色冲突("通用助手" vs "编排器")。
|
|
18
|
+
* - 唯一必须保留的是 **`hostAgent` 引用**(不是注册!):引擎用它的身份做
|
|
19
|
+
* `subagents.interrupt`/`followup` 的 authority/parent——DSH 机制要求,与工具注册无关。
|
|
20
|
+
*
|
|
21
|
+
* setup 只组合、不驱动(DSH「setup composes, never drives」),因此主代理创建后不会
|
|
22
|
+
* 自动发送首轮 prompt,编排仍由引擎 `runInstance` 主循环驱动。
|
|
23
|
+
* @module dsh-sandore/workflow/src/host-agent
|
|
24
|
+
*/
|
|
25
|
+
import type { AgentSetup } from '../microflow/subagents';
|
|
26
|
+
|
|
27
|
+
/** `ctx.agentPresets` 服务的最小面(由引擎 get 后传入工厂;签名对齐 DSH AgentPresets)。 */
|
|
28
|
+
export interface AgentPresetsSeam {
|
|
29
|
+
/** 挂载 preset 组合到 agent scope(无 id = 默认 preset)。 */
|
|
30
|
+
mount(agentCtx: unknown, id?: string): Promise<unknown>;
|
|
31
|
+
/** 解析 preset(无 id = 默认),返回含 `id` 的 preset 对象。 */
|
|
32
|
+
resolve(id?: string): Promise<{ readonly id: string }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 创建宿主主代理 setup 回调的入参。
|
|
37
|
+
*/
|
|
38
|
+
export interface CreateHostAgentSetupOptions {
|
|
39
|
+
/** 工作流实例 ID。 */
|
|
40
|
+
readonly instanceId: string;
|
|
41
|
+
/** `ctx.agentPresets` 服务(由引擎在 create 前 resolve 并传入;无 preset 部署时为 undefined)。 */
|
|
42
|
+
readonly presets?: AgentPresetsSeam;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 创建宿主主代理的 setup 回调(create 与 resume 共用,保证注册行为一致)。
|
|
47
|
+
*
|
|
48
|
+
* 返回的 `AgentSetup` 在 agent/session 发布前执行:挂载 DSH 默认 agent preset。
|
|
49
|
+
* `presets` 服务缺失时(无 preset 部署)跳过 mount,与 apiproxy `composeAgent` 的
|
|
50
|
+
* `presets === undefined` 分支一致。
|
|
51
|
+
* @param options - 实例 ID 与(可选的)agentPresets 服务。
|
|
52
|
+
* @returns 宿主主代理的 setup 回调。
|
|
53
|
+
*/
|
|
54
|
+
export function createHostAgentSetup(options: CreateHostAgentSetupOptions): AgentSetup {
|
|
55
|
+
const { presets } = options;
|
|
56
|
+
return async (agentCtx) => {
|
|
57
|
+
if (presets === undefined) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await presets.mount(agentCtx);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sandore 工作流引擎包入口。
|
|
3
|
+
*
|
|
4
|
+
* 默认导出 `SandoreWorkflowPlugin`(复合插件,见 `plugin.ts`)——它在
|
|
5
|
+
* `[Service.init]` 中注册 `SandorePersistence` / `SandoreEngineRuntime` /
|
|
6
|
+
* `SandoreRemote` 三个子服务,使 cordis.yml 单条目挂载即可提供
|
|
7
|
+
* `sandorePersistence` / `sandoreEngine` / `sandore` RPC。`SandoreEngine`
|
|
8
|
+
* 抽象 Service、13 个 `sandore/*` 事件双声明与 `SandoreEngineStub` 桩以
|
|
9
|
+
* named export 提供(见 `seam.ts`)。
|
|
10
|
+
*/
|
|
11
|
+
export type * from './types.ts'
|
|
12
|
+
export * from './errors.ts'
|
|
13
|
+
export * from './schema.ts'
|
|
14
|
+
export * from './domain.ts'
|
|
15
|
+
export * from './yaml-parser.ts'
|
|
16
|
+
export type * from './shell.ts'
|
|
17
|
+
export * from './seam.ts'
|
|
18
|
+
export * from './dag-navigator.ts'
|
|
19
|
+
export * from './remote.ts'
|
|
20
|
+
|
|
21
|
+
import SandoreEngineRuntime from './workflow-engine.ts';
|
|
22
|
+
import SandoreWorkflowPlugin from './plugin.ts';
|
|
23
|
+
|
|
24
|
+
export { SandoreEngineRuntime, SandoreWorkflowPlugin }
|
|
25
|
+
export default SandoreWorkflowPlugin
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interaction-manager.ts 的单元测试:引擎级 ask 主通道(登记 + 事件 + 卡片 await)、
|
|
3
|
+
* pending Map 镜像持久化、replyQuestion/replyPermission 兼容路径与 unknown
|
|
4
|
+
* requestId 报错、权限镜像(无自动审批)。
|
|
5
|
+
*
|
|
6
|
+
* 双通道 ask 重构(4.4)后不再测试 `[[SANDORE_ASK]]` 标记块解析/转接——该机制已随
|
|
7
|
+
* delegateSubagentAsk/parseDelegatedAsk 删除(子代理提问走 child-scope
|
|
8
|
+
* ask_user_question 工具 → 引擎 askFromSubagent → 本管理器 ask 主通道)。
|
|
9
|
+
* @module dsh-sandore/workflow/src/interaction-manager.spec
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { FakeAgent } from '../tests/mocks/agents.ts';
|
|
14
|
+
import { FakeUserQuestions } from '../tests/mocks/user-questions.ts';
|
|
15
|
+
import type { Agent } from '../microflow/subagents';
|
|
16
|
+
import {
|
|
17
|
+
InteractionManager,
|
|
18
|
+
isRejectedAnswer,
|
|
19
|
+
type InteractionManagerOptions,
|
|
20
|
+
type PermissionReply,
|
|
21
|
+
} from './interaction-manager.ts';
|
|
22
|
+
import type {
|
|
23
|
+
AskUserQuestionAnswer,
|
|
24
|
+
AskUserQuestionPayload,
|
|
25
|
+
PendingPermissionEntry,
|
|
26
|
+
PendingQuestionEntry,
|
|
27
|
+
} from './types.ts';
|
|
28
|
+
|
|
29
|
+
/** 构造一条 pending question 条目。 */
|
|
30
|
+
function questionEntry(requestId: string, payload: AskUserQuestionPayload): PendingQuestionEntry {
|
|
31
|
+
return {
|
|
32
|
+
id: requestId,
|
|
33
|
+
stageId: 'stg-1',
|
|
34
|
+
stepId: 'step-1',
|
|
35
|
+
sessionId: 'inst-1',
|
|
36
|
+
requestId,
|
|
37
|
+
kind: 'question',
|
|
38
|
+
status: 'pending',
|
|
39
|
+
payload,
|
|
40
|
+
createdAt: '2026-08-16T00:00:00.000Z',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 构造一条 pending permission 条目。 */
|
|
45
|
+
function permissionEntry(requestId: string): PendingPermissionEntry {
|
|
46
|
+
return {
|
|
47
|
+
id: requestId,
|
|
48
|
+
stageId: 'stg-1',
|
|
49
|
+
stepId: 'step-1',
|
|
50
|
+
sessionId: 'inst-1',
|
|
51
|
+
requestId,
|
|
52
|
+
kind: 'permission',
|
|
53
|
+
status: 'pending',
|
|
54
|
+
payload: { permission: 'write_file', patterns: ['/tmp/**'] },
|
|
55
|
+
createdAt: '2026-08-16T00:00:00.000Z',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 构造一个单题载荷(带选项)。 */
|
|
60
|
+
function singleQuestionPayload(): AskUserQuestionPayload {
|
|
61
|
+
return {
|
|
62
|
+
questions: [{
|
|
63
|
+
id: 'q1',
|
|
64
|
+
question: 'Which one?',
|
|
65
|
+
options: [{ label: 'A' }, { label: 'B', description: 'second' }],
|
|
66
|
+
}],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 构造一个整批回答。 */
|
|
71
|
+
function answer(selected: string[]): AskUserQuestionAnswer {
|
|
72
|
+
return { answers: [{ id: 'q1', selected }] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 组装测试依赖(hostAgent + FakeUserQuestions + vi.fn 桩)。 */
|
|
76
|
+
function makeDeps(options?: { userQuestions?: FakeUserQuestions; hostAgent?: Agent; reportToHost?: boolean }): {
|
|
77
|
+
deps: InteractionManagerOptions;
|
|
78
|
+
userQuestions: FakeUserQuestions;
|
|
79
|
+
hostAgent: Agent;
|
|
80
|
+
persist: ReturnType<typeof vi.fn>;
|
|
81
|
+
emitQuestion: ReturnType<typeof vi.fn>;
|
|
82
|
+
emitPermission: ReturnType<typeof vi.fn>;
|
|
83
|
+
emitQuestionRejected: ReturnType<typeof vi.fn>;
|
|
84
|
+
} {
|
|
85
|
+
const userQuestions = options?.userQuestions ?? new FakeUserQuestions();
|
|
86
|
+
const hostAgent = options?.hostAgent ?? new FakeAgent('host-1');
|
|
87
|
+
const persist = vi.fn();
|
|
88
|
+
const emitQuestion = vi.fn();
|
|
89
|
+
const emitPermission = vi.fn();
|
|
90
|
+
const emitQuestionRejected = vi.fn();
|
|
91
|
+
const deps: InteractionManagerOptions = {
|
|
92
|
+
hostAgent,
|
|
93
|
+
userQuestions: () => userQuestions,
|
|
94
|
+
// Round 26:默认上报宿主(场景 B,兼容既有用例);场景 A 用例显式传 false。
|
|
95
|
+
reportToHost: options?.reportToHost ?? true,
|
|
96
|
+
persist,
|
|
97
|
+
emitQuestion,
|
|
98
|
+
emitPermission,
|
|
99
|
+
emitQuestionRejected,
|
|
100
|
+
};
|
|
101
|
+
return { deps, userQuestions, hostAgent, persist, emitQuestion, emitPermission, emitQuestionRejected };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe('InteractionManager.addPendingQuestion / addPendingPermission', () => {
|
|
105
|
+
it('addPendingQuestion 登记即持久化(pending 条目)并发射 sandore/question', () => {
|
|
106
|
+
const { deps, persist, emitQuestion, emitPermission } = makeDeps();
|
|
107
|
+
const mgr = new InteractionManager(deps);
|
|
108
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
109
|
+
|
|
110
|
+
mgr.addPendingQuestion(entry);
|
|
111
|
+
|
|
112
|
+
expect(persist).toHaveBeenCalledWith([entry]);
|
|
113
|
+
expect(emitQuestion).toHaveBeenCalledWith(entry);
|
|
114
|
+
expect(emitPermission).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('addPendingPermission 登记即持久化(pending 条目)并发射 sandore/permission', () => {
|
|
118
|
+
const { deps, persist, emitPermission } = makeDeps();
|
|
119
|
+
const mgr = new InteractionManager(deps);
|
|
120
|
+
const entry = permissionEntry('req-p');
|
|
121
|
+
|
|
122
|
+
mgr.addPendingPermission(entry);
|
|
123
|
+
|
|
124
|
+
expect(persist).toHaveBeenCalledWith([entry]);
|
|
125
|
+
expect(emitPermission).toHaveBeenCalledWith(entry);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('InteractionManager.ask(引擎级 ask 主通道)', () => {
|
|
130
|
+
it('以 hostAgent 身份调用 userQuestions.ask,questions 对齐 AskUserQuestionItem(蛇形 multi_select → 驼峰 multiSelect)', async () => {
|
|
131
|
+
const { deps, userQuestions, hostAgent } = makeDeps();
|
|
132
|
+
const mgr = new InteractionManager(deps);
|
|
133
|
+
const payload: AskUserQuestionPayload = {
|
|
134
|
+
questions: [{
|
|
135
|
+
id: 'q1',
|
|
136
|
+
question: 'Which one?',
|
|
137
|
+
header: 'Choice',
|
|
138
|
+
options: [{ label: 'A' }],
|
|
139
|
+
multi_select: true,
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
const entry = questionEntry('req-1', payload);
|
|
143
|
+
const result = answer(['A']);
|
|
144
|
+
userQuestions.answers.push(result);
|
|
145
|
+
|
|
146
|
+
await expect(mgr.ask({ entry })).resolves.toEqual(result);
|
|
147
|
+
|
|
148
|
+
expect(userQuestions.calls).toHaveLength(1);
|
|
149
|
+
const request = userQuestions.calls[0]!;
|
|
150
|
+
expect(request.agent).toBe(hostAgent);
|
|
151
|
+
expect(request.questions).toEqual([{
|
|
152
|
+
id: 'q1',
|
|
153
|
+
question: 'Which one?',
|
|
154
|
+
header: 'Choice',
|
|
155
|
+
options: [{ label: 'A' }],
|
|
156
|
+
multiSelect: true,
|
|
157
|
+
}]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('卡片回复后条目移出 pending 并持久化空列表(终态条目只留内存审计)', async () => {
|
|
161
|
+
const { deps, userQuestions, persist } = makeDeps();
|
|
162
|
+
const mgr = new InteractionManager(deps);
|
|
163
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
164
|
+
userQuestions.answers.push(answer(['A']));
|
|
165
|
+
|
|
166
|
+
await mgr.ask({ entry });
|
|
167
|
+
|
|
168
|
+
// §3.6A P0#3:syncState 只持久化 pending;回复后 pending Map 清空 → 持久化 []。
|
|
169
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingQuestionEntry[];
|
|
170
|
+
expect(lastCall).toEqual([]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('无卡片通道时挂起,等待 replyQuestion 兼容路径 resolve(与 ask 的 await 汇合)', async () => {
|
|
174
|
+
const { deps, persist } = makeDeps();
|
|
175
|
+
const mgr = new InteractionManager(deps);
|
|
176
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
177
|
+
const result = answer(['A']);
|
|
178
|
+
|
|
179
|
+
const asking = mgr.ask({ entry });
|
|
180
|
+
await mgr.replyQuestion('req-1', result);
|
|
181
|
+
await expect(asking).resolves.toEqual(result);
|
|
182
|
+
|
|
183
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingQuestionEntry[];
|
|
184
|
+
expect(lastCall).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('InteractionManager.replyQuestion', () => {
|
|
189
|
+
it('未知 requestId 抛 SandoreError(INVALID_OPERATION)', async () => {
|
|
190
|
+
const { deps } = makeDeps();
|
|
191
|
+
const mgr = new InteractionManager(deps);
|
|
192
|
+
|
|
193
|
+
await expect(mgr.replyQuestion('nope', answer(['A']))).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('已 replied 的 requestId 再次回复抛 SandoreError(INVALID_OPERATION)', async () => {
|
|
197
|
+
const { deps, userQuestions } = makeDeps();
|
|
198
|
+
const mgr = new InteractionManager(deps);
|
|
199
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
200
|
+
userQuestions.answers.push(answer(['A']));
|
|
201
|
+
|
|
202
|
+
await mgr.ask({ entry });
|
|
203
|
+
await expect(mgr.replyQuestion('req-1', answer(['A']))).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('InteractionManager.replyPermission', () => {
|
|
208
|
+
it('reply=once/always 将条目移出 pending 并持久化空列表(终态条目只留内存审计)', async () => {
|
|
209
|
+
const { deps, persist } = makeDeps();
|
|
210
|
+
const mgr = new InteractionManager(deps);
|
|
211
|
+
const entry = permissionEntry('req-p');
|
|
212
|
+
mgr.addPendingPermission(entry);
|
|
213
|
+
|
|
214
|
+
await mgr.replyPermission('req-p', 'always');
|
|
215
|
+
|
|
216
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingPermissionEntry[];
|
|
217
|
+
expect(lastCall).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('reply=reject 将条目移出 pending 并持久化空列表(终态条目只留内存审计)', async () => {
|
|
221
|
+
const { deps, persist } = makeDeps();
|
|
222
|
+
const mgr = new InteractionManager(deps);
|
|
223
|
+
const entry = permissionEntry('req-p');
|
|
224
|
+
mgr.addPendingPermission(entry);
|
|
225
|
+
|
|
226
|
+
await mgr.replyPermission('req-p', 'reject');
|
|
227
|
+
|
|
228
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingPermissionEntry[];
|
|
229
|
+
expect(lastCall).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('未知 permission requestId 抛 SandoreError(INVALID_OPERATION)', async () => {
|
|
233
|
+
const { deps } = makeDeps();
|
|
234
|
+
const mgr = new InteractionManager(deps);
|
|
235
|
+
|
|
236
|
+
await expect(mgr.replyPermission('nope', 'once' as PermissionReply)).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('InteractionManager.rejectQuestion(缺陷 26:公开拒绝 → 终态化 + resolve 占位非 reject)', () => {
|
|
241
|
+
it('终态化(status rejected)+ 删 Map + resolve(非 reject)waiter 占位答案 + 持久化空列表 + 发 question_rejected', async () => {
|
|
242
|
+
const { deps, persist, emitQuestionRejected } = makeDeps();
|
|
243
|
+
const mgr = new InteractionManager(deps);
|
|
244
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
245
|
+
const asking = mgr.ask({ entry });
|
|
246
|
+
|
|
247
|
+
await mgr.rejectQuestion('req-1');
|
|
248
|
+
|
|
249
|
+
// waiter 以拒绝占位 resolve(非 reject——避免冒泡 runAndCatch 落实例 failed)。
|
|
250
|
+
const answer = await asking;
|
|
251
|
+
expect(answer).toEqual({ answers: [], rejected: true });
|
|
252
|
+
expect(isRejectedAnswer(answer)).toBe(true);
|
|
253
|
+
// 终态条目移出 pending Map → 持久化空列表(终态条目只留内存审计)。
|
|
254
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingQuestionEntry[];
|
|
255
|
+
expect(lastCall).toEqual([]);
|
|
256
|
+
// 双发 sandore/question_rejected(供 UI 刷新);载荷是终态化(rejected)条目。
|
|
257
|
+
expect(emitQuestionRejected).toHaveBeenCalledWith({ ...entry, status: 'rejected' });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('拒绝后条目不再 pending(再次 reject/reply 抛 INVALID_OPERATION)', async () => {
|
|
261
|
+
const { deps } = makeDeps();
|
|
262
|
+
const mgr = new InteractionManager(deps);
|
|
263
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
264
|
+
const asking = mgr.ask({ entry });
|
|
265
|
+
await mgr.rejectQuestion('req-1');
|
|
266
|
+
await asking;
|
|
267
|
+
|
|
268
|
+
await expect(mgr.rejectQuestion('req-1')).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
269
|
+
await expect(mgr.replyQuestion('req-1', answer(['A']))).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('未知 requestId 抛 SandoreError(INVALID_OPERATION)', async () => {
|
|
273
|
+
const { deps } = makeDeps();
|
|
274
|
+
const mgr = new InteractionManager(deps);
|
|
275
|
+
|
|
276
|
+
await expect(mgr.rejectQuestion('nope')).rejects.toMatchObject({ code: 'INVALID_OPERATION' });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('resolve 占位答案与 cancelMatching 的 CANCELLED_ANSWER 区分(带 rejected 标记)', async () => {
|
|
280
|
+
const { deps } = makeDeps();
|
|
281
|
+
const mgr = new InteractionManager(deps);
|
|
282
|
+
const entry = questionEntry('req-1', singleQuestionPayload());
|
|
283
|
+
const asking = mgr.ask({ entry });
|
|
284
|
+
|
|
285
|
+
await mgr.rejectQuestion('req-1');
|
|
286
|
+
const answer = await asking;
|
|
287
|
+
|
|
288
|
+
// 拒绝占位带 rejected:true 标记;clearAll/clearPendingInteractions 的 CANCELLED_ANSWER 无此标记。
|
|
289
|
+
expect(isRejectedAnswer(answer)).toBe(true);
|
|
290
|
+
expect(isRejectedAnswer({ answers: [] })).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ── 双通道 ask 重构(4.4)后:delegateSubagentAsk / parseDelegatedAsk 已随
|
|
295
|
+
// [[SANDORE_ASK]] 标记块机制删除——子代理提问走 child-scope ask_user_question 工具
|
|
296
|
+
// (引擎 askFromSubagent → 本管理器 ask 主通道,见 workflow-engine.spec.ts 的
|
|
297
|
+
// askFromSubagent 测试)。交互管理器保留的 ask 主通道/兼容回复/拒绝语义由上方
|
|
298
|
+
// 各 describe 覆盖(登记 + 事件 + 卡片 await + replyQuestion/rejectQuestion)。
|
|
299
|
+
|
|
300
|
+
describe('InteractionManager.clearPendingInteractions / clearAll(缺陷 27 + 缺陷 9 崩溃恢复)', () => {
|
|
301
|
+
it('clearPendingInteractions(stepId) 终态化匹配条目 + resolve waiter 占位答案 + 持久化剩余 pending', async () => {
|
|
302
|
+
const { deps, persist } = makeDeps();
|
|
303
|
+
const mgr = new InteractionManager(deps);
|
|
304
|
+
const step1Entry = questionEntry('req-1', singleQuestionPayload());
|
|
305
|
+
const step2Entry: PendingQuestionEntry = { ...questionEntry('req-2', singleQuestionPayload()), stepId: 'step-2' };
|
|
306
|
+
|
|
307
|
+
// 登记 step-1 的 ask(挂起 waiter)+ step-2 的 pending(不挂 waiter)。
|
|
308
|
+
const asking = mgr.ask({ entry: step1Entry });
|
|
309
|
+
mgr.addPendingQuestion(step2Entry);
|
|
310
|
+
|
|
311
|
+
mgr.clearPendingInteractions('step-1');
|
|
312
|
+
|
|
313
|
+
// step-1 waiter 以「已取消」占位答案 resolve(非 reject,避免冒泡 failed)。
|
|
314
|
+
await expect(asking).resolves.toEqual({ answers: [] });
|
|
315
|
+
// step-2 条目保留并重持久化(step-1 已清)。
|
|
316
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingQuestionEntry[];
|
|
317
|
+
expect(lastCall).toEqual([step2Entry]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('clearPendingInteractions 无匹配 stepId 时不触发 syncState(无 spurious 持久化)', async () => {
|
|
321
|
+
const { deps, persist } = makeDeps();
|
|
322
|
+
const mgr = new InteractionManager(deps);
|
|
323
|
+
mgr.addPendingQuestion(questionEntry('req-1', singleQuestionPayload()));
|
|
324
|
+
const callsBefore = persist.mock.calls.length;
|
|
325
|
+
|
|
326
|
+
mgr.clearPendingInteractions('step-missing');
|
|
327
|
+
|
|
328
|
+
expect(persist.mock.calls.length).toBe(callsBefore);
|
|
329
|
+
// 条目仍在(无匹配未清理):replyQuestion 可正常终态化。
|
|
330
|
+
await expect(mgr.replyQuestion('req-1', answer(['A']))).resolves.toBeUndefined();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('clearAll 终态化全部 pending(question+permission)+ resolve 全部 waiter + 持久化空列表', async () => {
|
|
334
|
+
const { deps, persist } = makeDeps();
|
|
335
|
+
const mgr = new InteractionManager(deps);
|
|
336
|
+
const qEntry = questionEntry('req-q', singleQuestionPayload());
|
|
337
|
+
const pEntry = permissionEntry('req-p');
|
|
338
|
+
const asking = mgr.ask({ entry: qEntry });
|
|
339
|
+
mgr.addPendingPermission(pEntry);
|
|
340
|
+
|
|
341
|
+
mgr.clearAll();
|
|
342
|
+
|
|
343
|
+
await expect(asking).resolves.toEqual({ answers: [] });
|
|
344
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingInteractionEntry[];
|
|
345
|
+
expect(lastCall).toEqual([]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('clearAll 在 Maps 为空时仍 syncState 覆盖 stale 快照 pendingInteractions(缺陷 9 崩溃恢复路径)', () => {
|
|
349
|
+
const { deps, persist } = makeDeps();
|
|
350
|
+
const mgr = new InteractionManager(deps);
|
|
351
|
+
|
|
352
|
+
// Maps 为空(崩溃恢复重建态):clearAll 仍触发 syncState 持久化空 pending。
|
|
353
|
+
mgr.clearAll();
|
|
354
|
+
|
|
355
|
+
expect(persist).toHaveBeenCalledWith([]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('场景 A(reportToHost=false):ask 不上报宿主(不调 userQuestions.ask),仅 replyQuestion 驱动', async () => {
|
|
359
|
+
const { deps, userQuestions, persist } = makeDeps({ reportToHost: false });
|
|
360
|
+
const mgr = new InteractionManager(deps);
|
|
361
|
+
const asking = mgr.ask({ entry: questionEntry('req-a', singleQuestionPayload()) });
|
|
362
|
+
|
|
363
|
+
// 不上报宿主:userQuestions.ask 不被调用(等 replyQuestion 兼容路径)。
|
|
364
|
+
await Promise.resolve();
|
|
365
|
+
expect(userQuestions.calls).toHaveLength(0);
|
|
366
|
+
|
|
367
|
+
await mgr.replyQuestion('req-a', answer(['A']));
|
|
368
|
+
await expect(asking).resolves.toEqual(answer(['A']));
|
|
369
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingInteractionEntry[];
|
|
370
|
+
expect(lastCall).toEqual([]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('场景 B(reportToHost=true):composer 回答(replyQuestion)后同步 abort 宿主 DSH 弹窗', async () => {
|
|
374
|
+
const { deps, userQuestions, persist } = makeDeps();
|
|
375
|
+
const mgr = new InteractionManager(deps);
|
|
376
|
+
const asking = mgr.ask({ entry: questionEntry('req-b', singleQuestionPayload()) });
|
|
377
|
+
|
|
378
|
+
await Promise.resolve();
|
|
379
|
+
expect(userQuestions.calls).toHaveLength(1);
|
|
380
|
+
const signal = userQuestions.calls[0]?.signal;
|
|
381
|
+
expect(signal?.aborted).toBe(false);
|
|
382
|
+
|
|
383
|
+
// composer 卡回答 → finishQuestion resolve 引擎 waiter + abort DSH 弹窗 signal。
|
|
384
|
+
await mgr.replyQuestion('req-b', answer(['B']));
|
|
385
|
+
await expect(asking).resolves.toEqual(answer(['B']));
|
|
386
|
+
expect(signal?.aborted).toBe(true);
|
|
387
|
+
const lastCall = persist.mock.calls[persist.mock.calls.length - 1]?.[0] as PendingInteractionEntry[];
|
|
388
|
+
expect(lastCall).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
});
|