airaknit 1.1.2-rc.9
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 +84 -0
- package/README.md +202 -0
- package/bin/airaknit +9 -0
- package/bin/airaknit-project +14 -0
- package/bin/kanna +9 -0
- package/dist/client/assets/CompactSummaryMessage-Yw0BDWEJ.js +1 -0
- package/dist/client/assets/ExitPlanModeMessage-DIdkQ4uF.js +1 -0
- package/dist/client/assets/LocalFilePreviewDialog-DQx2eiCc.js +3 -0
- package/dist/client/assets/LocalProjectsSection-C4xlWkgS.js +1 -0
- package/dist/client/assets/TextMessage-B5G39DEJ.js +1 -0
- package/dist/client/assets/UserMessage-CIkWk-0L.js +1 -0
- package/dist/client/assets/_basePickBy-CVrAFfnZ.js +1 -0
- package/dist/client/assets/_baseUniq-JL-aaF4P.js +1 -0
- package/dist/client/assets/arc-B07zg7ol.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-PSLVJL3p.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-DfWIF1G_.js +36 -0
- package/dist/client/assets/array-BGFCBI0e.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-CzwIeo_B.js +132 -0
- package/dist/client/assets/bundle-mjs-BDE2gWbQ.js +1 -0
- package/dist/client/assets/button-DO50qOGv.js +1 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-CR8DCQRE.js +10 -0
- package/dist/client/assets/channel-Dj-UUfaF.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-dznP-cn8.js +1 -0
- package/dist/client/assets/chunk-336JU56O-Dqss5Vu6.js +2 -0
- package/dist/client/assets/chunk-426QAEUC-CEKis_0_.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-BYOv0Gm1.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BxzubH5S.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BSlTj03a.js +1 -0
- package/dist/client/assets/chunk-5FUZZQ4R-9Au93Bi1.js +62 -0
- package/dist/client/assets/chunk-5PVQY5BW-BhksHFEZ.js +2 -0
- package/dist/client/assets/chunk-67CJDMHE-BFwhz-8t.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BDUPds87.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-CEWTdyXO.js +1 -0
- package/dist/client/assets/chunk-BO2N2NFS-D0LvxnhU.js +103 -0
- package/dist/client/assets/chunk-BSJP7CBP-BNJnK6sq.js +1 -0
- package/dist/client/assets/chunk-Bj-mKKzh.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-CYhfoCeN.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-C5ovJLc0.js +1 -0
- package/dist/client/assets/chunk-ENJZ2VHE-HOhYaeGr.js +10 -0
- package/dist/client/assets/chunk-FMBD7UC4-BLCiKcAQ.js +15 -0
- package/dist/client/assets/chunk-FOC6F5B3-B6GtY2ek.js +1 -0
- package/dist/client/assets/chunk-ICPOFSXX-DPoIZoC5.js +122 -0
- package/dist/client/assets/chunk-K5T4RW27-BsKN63rv.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-BUGn9uuY.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-DhaZyo03.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-DlpeeJyi.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-Dc9q7aYA.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-BEdrPoSb.js +1 -0
- package/dist/client/assets/chunk-U2HBQHQK-CIB3Bjjd.js +70 -0
- package/dist/client/assets/chunk-X2U36JSP-CtB-o8Yp.js +1 -0
- package/dist/client/assets/chunk-XPW4576I-C6iHhX_8.js +32 -0
- package/dist/client/assets/chunk-YZCP3GAM-CTmKr6ZH.js +1 -0
- package/dist/client/assets/chunk-ZZ45TVLE-BgU8A2RF.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-Bqk5e679.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-6pSaZOkC.js +1 -0
- package/dist/client/assets/client-BrKWI4CM.js +1 -0
- package/dist/client/assets/client-CGgNRU9w.js +1 -0
- package/dist/client/assets/client-DMSLRzg9.js +6 -0
- package/dist/client/assets/clone-DWcL7whJ.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CrV5wsV_.js +1 -0
- package/dist/client/assets/cytoscape.esm--aLzKuep.js +321 -0
- package/dist/client/assets/dagre-CuRxWcrj.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-BIDiVnkA.js +4 -0
- package/dist/client/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-i1kjKRCB.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-9ZSLuhbl.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-B4_CUjgv.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-Ct5eTGS8.js +24 -0
- package/dist/client/assets/dist-CuB4kiSK.js +1 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-Cy38ercc.js +85 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CZKuYl0V.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-DLPjCh7a.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-DqbrtEp9.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-BoRBkDhQ.js +106 -0
- package/dist/client/assets/graphlib-BcQ6qlQh.js +1 -0
- package/dist/client/assets/highlighted-body-OFNGDK62-BEpBVDTX.js +1 -0
- package/dist/client/assets/index-CetCiuqP.js +105 -0
- package/dist/client/assets/index-N29Mip7A.css +1 -0
- package/dist/client/assets/info-OMHHGYJF-D98DRBJX.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-BAcdTWbt.js +2 -0
- package/dist/client/assets/init-B8gtcn7T.js +1 -0
- package/dist/client/assets/isArrayLikeObject-D8SJFmkN.js +1 -0
- package/dist/client/assets/isEmpty-BF3YX5Jk.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-Ynu2VKdC.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-BjfhQaN3.js +139 -0
- package/dist/client/assets/jsx-runtime-CyI9ICYU.js +1 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-JLXH9zUJ.js +89 -0
- package/dist/client/assets/katex-B94qP8b6.js +265 -0
- package/dist/client/assets/lib--QVjyxmL.js +29 -0
- package/dist/client/assets/lib-B6rgJiZ9.js +1 -0
- package/dist/client/assets/line-DCrYfLBn.js +1 -0
- package/dist/client/assets/linear-_4upLmeo.js +1 -0
- package/dist/client/assets/mermaid-GHXKKRXX-rwJHYUmW.js +1 -0
- package/dist/client/assets/mermaid-parser.core-KZinfW8o.js +4 -0
- package/dist/client/assets/mermaid.core-QqY9gSNe.js +11 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-TWgHDAzp.js +96 -0
- package/dist/client/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-DEvfkn3F.js +1 -0
- package/dist/client/assets/path-DZF-JdEe.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-72e6WVjb.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-Cl8PCsoj.js +30 -0
- package/dist/client/assets/preload-helper-rov5CBGT.js +1 -0
- package/dist/client/assets/pty-client-DZ27IS00.js +1 -0
- package/dist/client/assets/ptyInstancesStore-D9ag7SYd.js +1 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CHyVGp9E.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-Cp7xd_EY.js +1 -0
- package/dist/client/assets/react-CClhXMB2.js +1 -0
- package/dist/client/assets/react-Dd6D81m0.js +1 -0
- package/dist/client/assets/react-dom--G6_6fQ_.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-DaanG2iM.js +84 -0
- package/dist/client/assets/rough.esm-BsmKo2S5.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-B_fhLY36.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-C5FNrveI.js +157 -0
- package/dist/client/assets/src-DeTlMJAU.js +1 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-nTTcdjjQ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dw0632j_.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-DkQV1yP8.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-ZLIgC7_K.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-BqaMbB6N.js +1 -0
- package/dist/client/assets/uiIdentityOverlay-Ba7GNj7m.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-DbZ2xgs6.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DXQS8zf4.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-BzCJ6MAu.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-BSlFecop.js +7 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/favicon.svg +15 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +31 -0
- package/dist/client/manifest.webmanifest +12 -0
- package/dist/client/sw.js +32 -0
- package/package.json +122 -0
- package/src/nats/auth-callout/callout-config.test.ts +93 -0
- package/src/nats/auth-callout/callout-config.ts +109 -0
- package/src/nats/auth-callout/callout.integration.test.ts +332 -0
- package/src/nats/auth-callout/keys.ts +103 -0
- package/src/nats/auth-callout/responder.ts +241 -0
- package/src/nats/auth-callout/scope-policy.test.ts +159 -0
- package/src/nats/auth-callout/scope-policy.ts +210 -0
- package/src/nats/auth-callout/token.test.ts +163 -0
- package/src/nats/auth-callout/token.ts +157 -0
- package/src/nats/nats-daemon-callout.ts +194 -0
- package/src/nats/nats-daemon.test.ts +77 -0
- package/src/nats/nats-daemon.ts +50 -0
- package/src/nats/nats-token.test.ts +61 -0
- package/src/nats/nats-token.ts +59 -0
- package/src/runner/coordination-mcp-integration.test.ts +134 -0
- package/src/runner/nats-coordination-client.test.ts +49 -0
- package/src/runner/nats-coordination-client.ts +94 -0
- package/src/runner/runner-agent.test.ts +469 -0
- package/src/runner/runner-agent.ts +453 -0
- package/src/runner/runner-credential.test.ts +93 -0
- package/src/runner/runner-credential.ts +82 -0
- package/src/runner/runner-nats.test.ts +495 -0
- package/src/runner/runner-nats.ts +323 -0
- package/src/runner/runner-pair.test.ts +107 -0
- package/src/runner/runner-pair.ts +81 -0
- package/src/runner/runner.test.ts +135 -0
- package/src/runner/runner.ts +212 -0
- package/src/runner/turn-factories.test.ts +97 -0
- package/src/runner/turn-factories.ts +475 -0
- package/src/server/agent-config-journey.test.ts +106 -0
- package/src/server/agent.ts +8 -0
- package/src/server/auto-continue/auth-error-detector.ts +66 -0
- package/src/server/auto-continue/limit-detector.ts +194 -0
- package/src/server/bm25.test.ts +92 -0
- package/src/server/bm25.ts +101 -0
- package/src/server/chat-events-jetstream.test.ts +135 -0
- package/src/server/claude-harness.ts +360 -0
- package/src/server/claude-pty/agent-normalizers.ts +309 -0
- package/src/server/claude-pty/auth.test.ts +38 -0
- package/src/server/claude-pty/auth.ts +32 -0
- package/src/server/claude-pty/claude-session-registry.adapter.ts +81 -0
- package/src/server/claude-pty/claude-session-registry.test.ts +149 -0
- package/src/server/claude-pty/driver.test.ts +902 -0
- package/src/server/claude-pty/driver.ts +807 -0
- package/src/server/claude-pty/jsonl-path.adapter.ts +57 -0
- package/src/server/claude-pty/jsonl-path.test.ts +114 -0
- package/src/server/claude-pty/jsonl-to-event.test.ts +241 -0
- package/src/server/claude-pty/jsonl-to-event.ts +174 -0
- package/src/server/claude-pty/output-ring.test.ts +35 -0
- package/src/server/claude-pty/output-ring.ts +25 -0
- package/src/server/claude-pty/parity-matrix.test.ts +227 -0
- package/src/server/claude-pty/pid-registry.adapter.ts +135 -0
- package/src/server/claude-pty/pid-registry.test.ts +122 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +20 -0
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +32 -0
- package/src/server/claude-pty/pty-instance-registry.test.ts +177 -0
- package/src/server/claude-pty/pty-instance-registry.ts +166 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +103 -0
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +85 -0
- package/src/server/claude-pty/pty-process.adapter.ts +66 -0
- package/src/server/claude-pty/pty-process.test.ts +49 -0
- package/src/server/claude-pty/resolve-binary.adapter.ts +106 -0
- package/src/server/claude-pty/resolve-binary.test.ts +118 -0
- package/src/server/claude-pty/runtime-dir.adapter.ts +19 -0
- package/src/server/claude-pty/settings-writer.adapter.ts +27 -0
- package/src/server/claude-pty/settings-writer.test.ts +22 -0
- package/src/server/claude-pty/smoke-test-io.adapter.ts +28 -0
- package/src/server/claude-pty/smoke-test.test.ts +191 -0
- package/src/server/claude-pty/smoke-test.ts +185 -0
- package/src/server/claude-pty/subagent-orchestrator.ts +887 -0
- package/src/server/claude-pty/tool-callback.ts +274 -0
- package/src/server/claude-pty/tui-control.test.ts +272 -0
- package/src/server/claude-pty/tui-control.ts +182 -0
- package/src/server/claude-pty/tui-source.adapter.test.ts +360 -0
- package/src/server/claude-pty/tui-source.adapter.ts +343 -0
- package/src/server/claude-pty/tunnel-gateway.ts +12 -0
- package/src/server/claude-pty-mcp/canonical-args.ts +15 -0
- package/src/server/claude-pty-mcp/fs-stat.adapter.ts +8 -0
- package/src/server/claude-pty-mcp/history-primer.ts +90 -0
- package/src/server/claude-pty-mcp/http-server.adapter.ts +33 -0
- package/src/server/claude-pty-mcp/mcp-http.ts +177 -0
- package/src/server/claude-pty-mcp/mcp.ts +412 -0
- package/src/server/claude-pty-mcp/mention-parser.ts +25 -0
- package/src/server/claude-pty-mcp/paths.ts +24 -0
- package/src/server/claude-pty-mcp/permission-gate.ts +243 -0
- package/src/server/claude-pty-mcp/terminal-pid-registry.adapter.ts +107 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.test.ts +119 -0
- package/src/server/claude-pty-mcp/tools/ask-user-question.ts +61 -0
- package/src/server/claude-pty-mcp/tools/bash.adapter.ts +76 -0
- package/src/server/claude-pty-mcp/tools/bash.test.ts +56 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.test.ts +155 -0
- package/src/server/claude-pty-mcp/tools/delegate-subagent.ts +111 -0
- package/src/server/claude-pty-mcp/tools/edit.adapter.ts +95 -0
- package/src/server/claude-pty-mcp/tools/edit.test.ts +93 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/exit-plan-mode.ts +50 -0
- package/src/server/claude-pty-mcp/tools/glob.adapter.ts +86 -0
- package/src/server/claude-pty-mcp/tools/glob.test.ts +61 -0
- package/src/server/claude-pty-mcp/tools/grep.adapter.ts +126 -0
- package/src/server/claude-pty-mcp/tools/grep.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/read.adapter.ts +58 -0
- package/src/server/claude-pty-mcp/tools/read.test.ts +62 -0
- package/src/server/claude-pty-mcp/tools/tool-callback-shim.ts +42 -0
- package/src/server/claude-pty-mcp/tools/webfetch.test.ts +81 -0
- package/src/server/claude-pty-mcp/tools/webfetch.ts +82 -0
- package/src/server/claude-pty-mcp/tools/websearch.test.ts +40 -0
- package/src/server/claude-pty-mcp/tools/websearch.ts +42 -0
- package/src/server/claude-pty-mcp/tools/write.adapter.ts +60 -0
- package/src/server/claude-pty-mcp/tools/write.test.ts +52 -0
- package/src/server/claude-pty-mcp/uploads.adapter.ts +98 -0
- package/src/server/claude-pty-mcp/uploads.ts +38 -0
- package/src/server/claude-turn.test.ts +176 -0
- package/src/server/cli-runtime.test.ts +456 -0
- package/src/server/cli-runtime.ts +374 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +78 -0
- package/src/server/client-log-forwarder.test.ts +74 -0
- package/src/server/client-log-forwarder.ts +75 -0
- package/src/server/codex-app-server-protocol.ts +449 -0
- package/src/server/codex-app-server.test.ts +2990 -0
- package/src/server/codex-app-server.ts +1713 -0
- package/src/server/coordination-integration.test.ts +63 -0
- package/src/server/coordination-mcp.test.ts +149 -0
- package/src/server/coordination-mcp.ts +197 -0
- package/src/server/delegation-coordinator.test.ts +675 -0
- package/src/server/delegation-coordinator.ts +454 -0
- package/src/server/discovery.test.ts +211 -0
- package/src/server/discovery.ts +301 -0
- package/src/server/event-store-agent-config.test.ts +124 -0
- package/src/server/event-store-coordination.test.ts +149 -0
- package/src/server/event-store-profile.test.ts +132 -0
- package/src/server/event-store-repo.test.ts +154 -0
- package/src/server/event-store-runner-team.test.ts +104 -0
- package/src/server/event-store.test.ts +342 -0
- package/src/server/event-store.ts +2208 -0
- package/src/server/events.ts +379 -0
- package/src/server/extension-router.test.ts +183 -0
- package/src/server/extension-router.ts +114 -0
- package/src/server/extensions/agents/server.test.ts +191 -0
- package/src/server/extensions/agents/server.ts +108 -0
- package/src/server/extensions/c3/server.test.ts +284 -0
- package/src/server/extensions/c3/server.ts +212 -0
- package/src/server/extensions/code/server.test.ts +200 -0
- package/src/server/extensions/code/server.ts +150 -0
- package/src/server/extensions.config.ts +10 -0
- package/src/server/external-open.ts +69 -0
- package/src/server/generate-fork-context.ts +58 -0
- package/src/server/generate-merge-context.test.ts +290 -0
- package/src/server/generate-merge-context.ts +141 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-clone-policy.test.ts +138 -0
- package/src/server/git-clone-policy.ts +27 -0
- package/src/server/harness-types.ts +1 -0
- package/src/server/journey-verification.test.ts +640 -0
- package/src/server/journey-verification.ts +195 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/nats-auth.test.ts +92 -0
- package/src/server/nats-auth.ts +6 -0
- package/src/server/nats-bind-guard.test.ts +71 -0
- package/src/server/nats-bind-guard.ts +42 -0
- package/src/server/nats-bridge.test.ts +141 -0
- package/src/server/nats-bridge.ts +111 -0
- package/src/server/nats-connector.test.ts +56 -0
- package/src/server/nats-connector.ts +71 -0
- package/src/server/nats-daemon-manager.test.ts +76 -0
- package/src/server/nats-daemon-manager.ts +174 -0
- package/src/server/nats-publisher.test.ts +356 -0
- package/src/server/nats-publisher.ts +271 -0
- package/src/server/nats-responders.test.ts +1018 -0
- package/src/server/nats-responders.ts +925 -0
- package/src/server/nats-streams.test.ts +112 -0
- package/src/server/nats-streams.ts +129 -0
- package/src/server/oauth-pool/oauth-responders.ts +185 -0
- package/src/server/oauth-pool/oauth-settings-store.ts +173 -0
- package/src/server/oauth-pool/oauth-token-pool.ts +303 -0
- package/src/server/orchestration.test.ts +1063 -0
- package/src/server/orchestration.ts +716 -0
- package/src/server/pairing-endpoints.test.ts +171 -0
- package/src/server/pairing-store.test.ts +154 -0
- package/src/server/pairing-store.ts +124 -0
- package/src/server/paths.ts +27 -0
- package/src/server/pr3-liveness.test.ts +252 -0
- package/src/server/process-utils.ts +10 -0
- package/src/server/project-cli.ts +180 -0
- package/src/server/provider-catalog.test.ts +177 -0
- package/src/server/provider-catalog.ts +146 -0
- package/src/server/pty-responders.ts +345 -0
- package/src/server/push-notifications.test.ts +234 -0
- package/src/server/push-notifications.ts +188 -0
- package/src/server/quick-response.test.ts +196 -0
- package/src/server/quick-response.ts +154 -0
- package/src/server/read-models-agent-config.test.ts +59 -0
- package/src/server/read-models-coordination.test.ts +69 -0
- package/src/server/read-models.test.ts +332 -0
- package/src/server/read-models.ts +258 -0
- package/src/server/repo-journey.test.ts +96 -0
- package/src/server/repo-manager.test.ts +118 -0
- package/src/server/repo-manager.ts +97 -0
- package/src/server/repo-status.test.ts +54 -0
- package/src/server/repo-status.ts +82 -0
- package/src/server/restart.test.ts +27 -0
- package/src/server/restart.ts +30 -0
- package/src/server/runner-incompatible-gate.test.ts +383 -0
- package/src/server/runner-manager.test.ts +72 -0
- package/src/server/runner-manager.ts +312 -0
- package/src/server/runner-pairing-urls.test.ts +59 -0
- package/src/server/runner-pairing-urls.ts +67 -0
- package/src/server/runner-proxy.test.ts +456 -0
- package/src/server/runner-proxy.ts +494 -0
- package/src/server/runner-router.test.ts +429 -0
- package/src/server/runner-router.ts +212 -0
- package/src/server/runner-routing.test.ts +584 -0
- package/src/server/runtime-registry.test.ts +436 -0
- package/src/server/runtime-registry.ts +307 -0
- package/src/server/sandbox-health.test.ts +127 -0
- package/src/server/sandbox-health.ts +94 -0
- package/src/server/sandbox-journey.test.ts +232 -0
- package/src/server/sandbox-manager.test.ts +146 -0
- package/src/server/sandbox-manager.ts +159 -0
- package/src/server/server.test.ts +287 -0
- package/src/server/server.ts +1108 -0
- package/src/server/session-discovery.test.ts +129 -0
- package/src/server/session-discovery.ts +475 -0
- package/src/server/session-index.test.ts +362 -0
- package/src/server/session-index.ts +119 -0
- package/src/server/session-seed.ts +288 -0
- package/src/server/share.test.ts +108 -0
- package/src/server/share.ts +113 -0
- package/src/server/skill-discovery.test.ts +201 -0
- package/src/server/skill-discovery.ts +77 -0
- package/src/server/storage/test-helpers.ts +67 -0
- package/src/server/terminal-manager.test.ts +309 -0
- package/src/server/terminal-manager.ts +354 -0
- package/src/server/transcript-consumer.test.ts +339 -0
- package/src/server/transcript-consumer.ts +162 -0
- package/src/server/transcript-search.test.ts +193 -0
- package/src/server/transcript-search.ts +83 -0
- package/src/server/transcript-utils.ts +52 -0
- package/src/server/update-manager.test.ts +107 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/workflow-engine.test.ts +251 -0
- package/src/server/workflow-engine.ts +169 -0
- package/src/server/workflow-mcp.ts +49 -0
- package/src/server/workflow-store.test.ts +155 -0
- package/src/server/workflow-store.ts +139 -0
- package/src/server/workspace-agent-integration.test.ts +167 -0
- package/src/server/workspace-agent-routes.test.ts +127 -0
- package/src/server/workspace-agent-routes.ts +89 -0
- package/src/server/workspace-agent.test.ts +103 -0
- package/src/server/workspace-agent.ts +102 -0
- package/src/server/workspace-cli.test.ts +79 -0
- package/src/server/workspace-config-manager.test.ts +109 -0
- package/src/server/workspace-config-manager.ts +83 -0
- package/src/server/workspace-directory-policy.test.ts +109 -0
- package/src/server/workspace-directory-policy.ts +56 -0
- package/src/shared/agent-config-types.ts +25 -0
- package/src/shared/branding.test.ts +42 -0
- package/src/shared/branding.ts +54 -0
- package/src/shared/compression.test.ts +85 -0
- package/src/shared/compression.ts +42 -0
- package/src/shared/coordination-store.test.ts +24 -0
- package/src/shared/coordination-store.ts +26 -0
- package/src/shared/dev-ports.test.ts +84 -0
- package/src/shared/dev-ports.ts +100 -0
- package/src/shared/extension-types.ts +45 -0
- package/src/shared/fork-presets.ts +54 -0
- package/src/shared/harness-types.test.ts +15 -0
- package/src/shared/harness-types.ts +21 -0
- package/src/shared/log-sink.test.ts +112 -0
- package/src/shared/log-sink.ts +185 -0
- package/src/shared/mention-pattern.ts +7 -0
- package/src/shared/merge-presets.ts +41 -0
- package/src/shared/nats-subjects.test.ts +61 -0
- package/src/shared/nats-subjects.ts +131 -0
- package/src/shared/permission-policy.ts +136 -0
- package/src/shared/ports.ts +3 -0
- package/src/shared/preset-types.ts +15 -0
- package/src/shared/profile-types.ts +49 -0
- package/src/shared/projectFileUrl.ts +36 -0
- package/src/shared/protocol.ts +153 -0
- package/src/shared/pty-instance.ts +43 -0
- package/src/shared/puggy/diagnostics/result.ts +18 -0
- package/src/shared/puggy/expressions/evaluate.ts +292 -0
- package/src/shared/puggy/index.test.ts +101 -0
- package/src/shared/puggy/index.ts +69 -0
- package/src/shared/puggy/parser/ast.ts +110 -0
- package/src/shared/puggy/parser/parser.ts +624 -0
- package/src/shared/puggy/renderer/html.ts +447 -0
- package/src/shared/runner-protocol.test.ts +277 -0
- package/src/shared/runner-protocol.ts +210 -0
- package/src/shared/runner-team-types.ts +73 -0
- package/src/shared/runtime-types.ts +48 -0
- package/src/shared/sandbox-types.ts +53 -0
- package/src/shared/tailwind-build.test.ts +12 -0
- package/src/shared/tinkaria-system-prompt.ts +101 -0
- package/src/shared/tools.test.ts +335 -0
- package/src/shared/tools.ts +397 -0
- package/src/shared/transcript-entries.ts +27 -0
- package/src/shared/transcript-render.test.ts +225 -0
- package/src/shared/transcript-render.ts +467 -0
- package/src/shared/types.ts +1031 -0
- package/src/shared/vite-config.test.ts +47 -0
- package/src/shared/web-context.test.ts +110 -0
- package/src/shared/web-context.ts +76 -0
- package/src/shared/workflow-types.ts +51 -0
- package/src/shared/workspace-types.ts +100 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import { afterEach, describe, test, expect } from "bun:test"
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { NatsServer } from "@lagz0ne/nats-embedded"
|
|
6
|
+
import { connect, type NatsConnection } from "@nats-io/transport-node"
|
|
7
|
+
import { registerCommandResponders, type RegisterRespondersArgs } from "./nats-responders"
|
|
8
|
+
import { commandSubject } from "../shared/nats-subjects"
|
|
9
|
+
import { EventStore } from "./event-store"
|
|
10
|
+
|
|
11
|
+
const encoder = new TextEncoder()
|
|
12
|
+
const decoder = new TextDecoder()
|
|
13
|
+
|
|
14
|
+
function encode(data: unknown): Uint8Array {
|
|
15
|
+
return encoder.encode(JSON.stringify(data))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function decode<T>(data: Uint8Array): T {
|
|
19
|
+
return JSON.parse(decoder.decode(data)) as T
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CommandResponse {
|
|
23
|
+
ok: boolean
|
|
24
|
+
result?: unknown
|
|
25
|
+
error?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- Mocks ---
|
|
29
|
+
|
|
30
|
+
function createMockStore() {
|
|
31
|
+
return {
|
|
32
|
+
state: {},
|
|
33
|
+
openProject: async (_path: string, _title?: string) => ({ id: "proj-1" }),
|
|
34
|
+
getProject: (id: string) => (id === "proj-1" ? { id: "proj-1", localPath: "/tmp/test-project" } : null),
|
|
35
|
+
getChat: (id: string) => (id === "chat-1"
|
|
36
|
+
? { id: "chat-1", workspaceId: "proj-1", provider: "codex", sessionToken: "session-1" }
|
|
37
|
+
: null),
|
|
38
|
+
removeProject: async () => {},
|
|
39
|
+
createChat: async (_workspaceId: string) => ({ id: "chat-1" }),
|
|
40
|
+
renameChat: async () => {},
|
|
41
|
+
deleteChat: async () => {},
|
|
42
|
+
listChatsByProject: () => [{ id: "chat-1" }],
|
|
43
|
+
getMessages: () => [],
|
|
44
|
+
getMessageCount: async () => 0,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createMockAgent() {
|
|
49
|
+
return {
|
|
50
|
+
send: async () => ({ chatId: "chat-1" }),
|
|
51
|
+
queue: async () => ({ chatId: "chat-1", queued: true }),
|
|
52
|
+
cancel: async () => {},
|
|
53
|
+
disposeChat: async () => {},
|
|
54
|
+
respondTool: async () => {},
|
|
55
|
+
getActiveStatuses: () => new Map(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createMockTerminals() {
|
|
60
|
+
return {
|
|
61
|
+
createTerminal: (opts: { terminalId: string }) => ({
|
|
62
|
+
terminalId: opts.terminalId,
|
|
63
|
+
title: "bash",
|
|
64
|
+
cwd: "/tmp",
|
|
65
|
+
shell: "/bin/bash",
|
|
66
|
+
cols: 80,
|
|
67
|
+
rows: 24,
|
|
68
|
+
scrollback: 1000,
|
|
69
|
+
serializedState: "",
|
|
70
|
+
status: "running",
|
|
71
|
+
exitCode: null,
|
|
72
|
+
}),
|
|
73
|
+
write: () => {},
|
|
74
|
+
resize: () => {},
|
|
75
|
+
close: () => {},
|
|
76
|
+
closeByCwd: () => {},
|
|
77
|
+
getSnapshot: () => null,
|
|
78
|
+
onEvent: () => () => {},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createMockUpdateManager() {
|
|
83
|
+
const snapshot = {
|
|
84
|
+
currentVersion: "1.0.0",
|
|
85
|
+
latestVersion: "1.1.0",
|
|
86
|
+
status: "available" as const,
|
|
87
|
+
updateAvailable: true,
|
|
88
|
+
lastCheckedAt: Date.now(),
|
|
89
|
+
error: null,
|
|
90
|
+
installAction: "restart" as const,
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
checkForUpdates: async () => snapshot,
|
|
94
|
+
installUpdate: async () => ({ success: true, version: "1.1.0" }),
|
|
95
|
+
getSnapshot: () => snapshot,
|
|
96
|
+
onChange: () => () => {},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Test infrastructure ---
|
|
101
|
+
|
|
102
|
+
let server: NatsServer | null = null
|
|
103
|
+
let serverNc: NatsConnection | null = null
|
|
104
|
+
let clientNc: NatsConnection | null = null
|
|
105
|
+
let disposeFn: (() => void) | null = null
|
|
106
|
+
let tempDir: string | null = null
|
|
107
|
+
|
|
108
|
+
afterEach(async () => {
|
|
109
|
+
disposeFn?.()
|
|
110
|
+
disposeFn = null
|
|
111
|
+
if (clientNc) {
|
|
112
|
+
await clientNc.drain()
|
|
113
|
+
clientNc = null
|
|
114
|
+
}
|
|
115
|
+
if (serverNc) {
|
|
116
|
+
await serverNc.drain()
|
|
117
|
+
serverNc = null
|
|
118
|
+
}
|
|
119
|
+
if (server) {
|
|
120
|
+
await server.stop()
|
|
121
|
+
server = null
|
|
122
|
+
}
|
|
123
|
+
if (tempDir) {
|
|
124
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
125
|
+
tempDir = null
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
async function setup(overrides?: Partial<RegisterRespondersArgs>) {
|
|
130
|
+
server = await NatsServer.start()
|
|
131
|
+
serverNc = await connect({ servers: server.url })
|
|
132
|
+
clientNc = await connect({ servers: server.url })
|
|
133
|
+
|
|
134
|
+
const args: RegisterRespondersArgs = {
|
|
135
|
+
nc: serverNc,
|
|
136
|
+
store: createMockStore() as never,
|
|
137
|
+
agent: createMockAgent() as never,
|
|
138
|
+
terminals: createMockTerminals() as never,
|
|
139
|
+
refreshDiscovery: async () => [],
|
|
140
|
+
getDiscoveredProjects: () => [],
|
|
141
|
+
machineDisplayName: "Test Machine",
|
|
142
|
+
updateManager: createMockUpdateManager() as never,
|
|
143
|
+
publisher: {
|
|
144
|
+
addSubscription: () => {},
|
|
145
|
+
removeSubscription: () => {},
|
|
146
|
+
getSnapshot: async () => null,
|
|
147
|
+
broadcastSnapshots: async () => {},
|
|
148
|
+
publishChatMessage: () => {},
|
|
149
|
+
dispose: () => {},
|
|
150
|
+
},
|
|
151
|
+
onStateChange: () => {},
|
|
152
|
+
directoryPolicy: null,
|
|
153
|
+
repoManager: null,
|
|
154
|
+
clonePolicy: null,
|
|
155
|
+
workflowEngine: null,
|
|
156
|
+
workflowStore: null,
|
|
157
|
+
sandboxManager: null,
|
|
158
|
+
runtimeRegistry: null,
|
|
159
|
+
...overrides,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { dispose } = registerCommandResponders(args)
|
|
163
|
+
disposeFn = dispose
|
|
164
|
+
|
|
165
|
+
// Allow subscription to propagate
|
|
166
|
+
await serverNc.flush()
|
|
167
|
+
|
|
168
|
+
return { clientNc: clientNc!, args }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function sendCommand(nc: NatsConnection, command: unknown): Promise<CommandResponse> {
|
|
172
|
+
const msg = await nc.request(commandSubject((command as { type: string }).type), encode(command), { timeout: 2000 })
|
|
173
|
+
return decode<CommandResponse>(msg.data)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Tests ---
|
|
177
|
+
|
|
178
|
+
describe("nats-responders", () => {
|
|
179
|
+
test("system.ping responds ok", async () => {
|
|
180
|
+
const { clientNc } = await setup()
|
|
181
|
+
const res = await sendCommand(clientNc, { type: "system.ping" })
|
|
182
|
+
expect(res.ok).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("system.ping does not trigger onStateChange", async () => {
|
|
186
|
+
let changed = false
|
|
187
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
188
|
+
await sendCommand(clientNc, { type: "system.ping" })
|
|
189
|
+
expect(changed).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
test("system.readLocalFilePreview returns local file content", async () => {
|
|
194
|
+
tempDir = await mkdtemp(path.join(tmpdir(), "kanna-preview-"))
|
|
195
|
+
const filePath = path.join(tempDir, "README.md")
|
|
196
|
+
await writeFile(filePath, "# Preview\n")
|
|
197
|
+
|
|
198
|
+
const { clientNc } = await setup()
|
|
199
|
+
const res = await sendCommand(clientNc, { type: "system.readLocalFilePreview", localPath: filePath })
|
|
200
|
+
|
|
201
|
+
expect(res.ok).toBe(true)
|
|
202
|
+
expect(res.result).toEqual({
|
|
203
|
+
localPath: filePath,
|
|
204
|
+
content: "# Preview\n",
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("system.readLocalFilePreview does not trigger onStateChange", async () => {
|
|
209
|
+
tempDir = await mkdtemp(path.join(tmpdir(), "kanna-preview-"))
|
|
210
|
+
const filePath = path.join(tempDir, "README.md")
|
|
211
|
+
await writeFile(filePath, "# Preview\n")
|
|
212
|
+
|
|
213
|
+
let changed = false
|
|
214
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
215
|
+
await sendCommand(clientNc, { type: "system.readLocalFilePreview", localPath: filePath })
|
|
216
|
+
expect(changed).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test("chat.getSessionRuntime returns null when the session file cannot be inspected", async () => {
|
|
220
|
+
const { clientNc } = await setup()
|
|
221
|
+
const res = await sendCommand(clientNc, { type: "chat.getSessionRuntime", chatId: "chat-1" })
|
|
222
|
+
|
|
223
|
+
expect(res.ok).toBe(true)
|
|
224
|
+
expect(res.result).toEqual({ runtime: null })
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test("chat.getSessionRuntime does not trigger onStateChange", async () => {
|
|
228
|
+
let changed = false
|
|
229
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
230
|
+
await sendCommand(clientNc, { type: "chat.getSessionRuntime", chatId: "chat-1" })
|
|
231
|
+
expect(changed).toBe(false)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test("chat.generateForkPrompt returns a derived prompt from transcript context", async () => {
|
|
235
|
+
const generateCalls: Array<{ intent: string; entries: unknown[]; cwd: string; preset?: string }> = []
|
|
236
|
+
const { clientNc } = await setup({
|
|
237
|
+
store: {
|
|
238
|
+
...createMockStore(),
|
|
239
|
+
getMessages: () => [
|
|
240
|
+
{ kind: "user_prompt", content: "Investigate auth race", _id: "1", createdAt: 1 },
|
|
241
|
+
{ kind: "assistant_text", text: "Likely around session restore", _id: "2", createdAt: 2 },
|
|
242
|
+
],
|
|
243
|
+
} as never,
|
|
244
|
+
generateForkPrompt: async (intent, entries, cwd, preset) => {
|
|
245
|
+
generateCalls.push({ intent, entries, cwd, preset })
|
|
246
|
+
return "## Objective\nFix the auth race"
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const res = await sendCommand(clientNc, {
|
|
251
|
+
type: "chat.generateForkPrompt",
|
|
252
|
+
chatId: "chat-1",
|
|
253
|
+
intent: "Focus on the regression test",
|
|
254
|
+
preset: "tests",
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(res.ok).toBe(true)
|
|
258
|
+
expect(res.result).toEqual({ prompt: "## Objective\nFix the auth race" })
|
|
259
|
+
expect(generateCalls).toEqual([
|
|
260
|
+
{
|
|
261
|
+
intent: "Focus on the regression test",
|
|
262
|
+
entries: [
|
|
263
|
+
{ kind: "user_prompt", content: "Investigate auth race", _id: "1", createdAt: 1 },
|
|
264
|
+
{ kind: "assistant_text", text: "Likely around session restore", _id: "2", createdAt: 2 },
|
|
265
|
+
],
|
|
266
|
+
cwd: "/tmp/test-project",
|
|
267
|
+
preset: "tests",
|
|
268
|
+
},
|
|
269
|
+
])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test("chat.generateMergePrompt returns a synthesized prompt from multiple sessions", async () => {
|
|
273
|
+
const mergeCalls: Array<{ intent: string; sessions: Array<{ chatId: string; entries: unknown[] }>; cwd: string; preset?: string }> = []
|
|
274
|
+
const multiChatStore = {
|
|
275
|
+
...createMockStore(),
|
|
276
|
+
getChat: (id: string) => {
|
|
277
|
+
if (id === "chat-1" || id === "chat-2") return { id, workspaceId: "proj-1", provider: "claude", sessionToken: `session-${id}` }
|
|
278
|
+
return null
|
|
279
|
+
},
|
|
280
|
+
getMessages: (chatId: string) => {
|
|
281
|
+
if (chatId === "chat-1") return [{ kind: "user_prompt", content: "Session 1 work", _id: "1", createdAt: 1 }]
|
|
282
|
+
if (chatId === "chat-2") return [{ kind: "assistant_text", text: "Session 2 output", _id: "2", createdAt: 2 }]
|
|
283
|
+
return []
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { clientNc } = await setup({
|
|
288
|
+
store: multiChatStore as never,
|
|
289
|
+
generateMergePrompt: async (intent, sessions, cwd, preset) => {
|
|
290
|
+
mergeCalls.push({ intent, sessions, cwd, preset })
|
|
291
|
+
return "## Merged\nCombined context from sessions"
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const res = await sendCommand(clientNc, {
|
|
296
|
+
type: "chat.generateMergePrompt",
|
|
297
|
+
chatIds: ["chat-1", "chat-2"],
|
|
298
|
+
intent: "Synthesize findings",
|
|
299
|
+
preset: "synthesis",
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
expect(res.ok).toBe(true)
|
|
303
|
+
expect(res.result).toEqual({ prompt: "## Merged\nCombined context from sessions" })
|
|
304
|
+
expect(mergeCalls).toHaveLength(1)
|
|
305
|
+
expect(mergeCalls[0]!.intent).toBe("Synthesize findings")
|
|
306
|
+
expect(mergeCalls[0]!.sessions).toHaveLength(2)
|
|
307
|
+
expect(mergeCalls[0]!.cwd).toBe("/tmp/test-project")
|
|
308
|
+
expect(mergeCalls[0]!.preset).toBe("synthesis")
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test("chat.generateMergePrompt rejects empty chatIds", async () => {
|
|
312
|
+
const { clientNc } = await setup({
|
|
313
|
+
generateMergePrompt: async () => "should not reach",
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
const res = await sendCommand(clientNc, {
|
|
317
|
+
type: "chat.generateMergePrompt",
|
|
318
|
+
chatIds: [],
|
|
319
|
+
intent: "Merge this",
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
expect(res.ok).toBe(false)
|
|
323
|
+
expect(res.error).toContain("At least 1 session")
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test("chat.generateMergePrompt does not trigger onStateChange", async () => {
|
|
327
|
+
let changed = false
|
|
328
|
+
const multiChatStore = {
|
|
329
|
+
...createMockStore(),
|
|
330
|
+
getChat: (id: string) => {
|
|
331
|
+
if (id === "chat-1" || id === "chat-2") return { id, workspaceId: "proj-1", provider: "claude", sessionToken: `session-${id}` }
|
|
332
|
+
return null
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
const { clientNc } = await setup({
|
|
336
|
+
store: multiChatStore as never,
|
|
337
|
+
onStateChange: () => { changed = true },
|
|
338
|
+
generateMergePrompt: async () => "merge seed",
|
|
339
|
+
})
|
|
340
|
+
await sendCommand(clientNc, { type: "chat.generateMergePrompt", chatIds: ["chat-1", "chat-2"], intent: "Merge these" })
|
|
341
|
+
expect(changed).toBe(false)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test("chat.generateForkPrompt does not trigger onStateChange", async () => {
|
|
345
|
+
let changed = false
|
|
346
|
+
const { clientNc } = await setup({
|
|
347
|
+
onStateChange: () => { changed = true },
|
|
348
|
+
generateForkPrompt: async () => "fork seed",
|
|
349
|
+
})
|
|
350
|
+
await sendCommand(clientNc, { type: "chat.generateForkPrompt", chatId: "chat-1", intent: "Fork this work" })
|
|
351
|
+
expect(changed).toBe(false)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test("chat.getRepoStatus returns repo status for the active project", async () => {
|
|
355
|
+
tempDir = await mkdtemp(path.join(tmpdir(), "kanna-repo-status-"))
|
|
356
|
+
const { clientNc } = await setup({
|
|
357
|
+
store: {
|
|
358
|
+
...createMockStore(),
|
|
359
|
+
getProject: (id: string) => (id === "proj-1" ? { id: "proj-1", localPath: tempDir! } : null),
|
|
360
|
+
} as never,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const res = await sendCommand(clientNc, { type: "chat.getRepoStatus", chatId: "chat-1" })
|
|
364
|
+
|
|
365
|
+
expect(res.ok).toBe(true)
|
|
366
|
+
expect(res.result).toEqual({
|
|
367
|
+
repoStatus: {
|
|
368
|
+
localPath: tempDir,
|
|
369
|
+
branch: null,
|
|
370
|
+
stagedCount: 0,
|
|
371
|
+
unstagedCount: 0,
|
|
372
|
+
untrackedCount: 0,
|
|
373
|
+
ahead: 0,
|
|
374
|
+
behind: 0,
|
|
375
|
+
isRepo: false,
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test("chat.getRepoStatus does not trigger onStateChange", async () => {
|
|
381
|
+
let changed = false
|
|
382
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
383
|
+
await sendCommand(clientNc, { type: "chat.getRepoStatus", chatId: "chat-1" })
|
|
384
|
+
expect(changed).toBe(false)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
test("update.check returns snapshot when manager exists", async () => {
|
|
389
|
+
const { clientNc } = await setup()
|
|
390
|
+
const res = await sendCommand(clientNc, { type: "update.check" })
|
|
391
|
+
expect(res.ok).toBe(true)
|
|
392
|
+
const result = res.result as { currentVersion: string; updateAvailable: boolean }
|
|
393
|
+
expect(result.currentVersion).toBe("1.0.0")
|
|
394
|
+
expect(result.updateAvailable).toBe(true)
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test("update.check returns fallback when manager is null", async () => {
|
|
398
|
+
const { clientNc } = await setup({ updateManager: null })
|
|
399
|
+
const res = await sendCommand(clientNc, { type: "update.check" })
|
|
400
|
+
expect(res.ok).toBe(true)
|
|
401
|
+
const result = res.result as { currentVersion: string; status: string }
|
|
402
|
+
expect(result.currentVersion).toBe("unknown")
|
|
403
|
+
expect(result.status).toBe("error")
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
test("update.check does not trigger onStateChange", async () => {
|
|
407
|
+
let changed = false
|
|
408
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
409
|
+
await sendCommand(clientNc, { type: "update.check" })
|
|
410
|
+
expect(changed).toBe(false)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test("update.install returns result", async () => {
|
|
414
|
+
const { clientNc } = await setup()
|
|
415
|
+
const res = await sendCommand(clientNc, { type: "update.install" })
|
|
416
|
+
expect(res.ok).toBe(true)
|
|
417
|
+
expect(res.result).toEqual({ success: true, version: "1.1.0" })
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test("update.install errors when manager is null", async () => {
|
|
421
|
+
const { clientNc } = await setup({ updateManager: null })
|
|
422
|
+
const res = await sendCommand(clientNc, { type: "update.install" })
|
|
423
|
+
expect(res.ok).toBe(false)
|
|
424
|
+
expect(res.error).toBe("Update manager unavailable.")
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test("chat.create returns chatId and triggers onStateChange", async () => {
|
|
428
|
+
let changed = false
|
|
429
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
430
|
+
const res = await sendCommand(clientNc, { type: "chat.create", workspaceId: "proj-1" })
|
|
431
|
+
expect(res.ok).toBe(true)
|
|
432
|
+
expect(res.result).toEqual({ chatId: "chat-1" })
|
|
433
|
+
expect(changed).toBe(true)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
test("chat.create forwards a client-supplied optimistic chatId", async () => {
|
|
437
|
+
let seenWorkspaceId: string | null = null
|
|
438
|
+
let seenChatId: string | null = null
|
|
439
|
+
const { clientNc } = await setup({
|
|
440
|
+
store: {
|
|
441
|
+
...createMockStore(),
|
|
442
|
+
createChat: async (workspaceId: string, _repoId?: string, chatId?: string) => {
|
|
443
|
+
seenWorkspaceId = workspaceId
|
|
444
|
+
seenChatId = chatId ?? null
|
|
445
|
+
return { id: chatId ?? "chat-1", workspaceId }
|
|
446
|
+
},
|
|
447
|
+
} as never,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const res = await sendCommand(clientNc, {
|
|
451
|
+
type: "chat.create",
|
|
452
|
+
workspaceId: "proj-1",
|
|
453
|
+
chatId: "chat-optimistic-1",
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
expect(res.ok).toBe(true)
|
|
457
|
+
expect(res.result).toEqual({ chatId: "chat-optimistic-1" })
|
|
458
|
+
if (seenWorkspaceId === null || seenChatId === null) {
|
|
459
|
+
throw new Error("expected chat.create to forward the optimistic identifiers")
|
|
460
|
+
}
|
|
461
|
+
expect(String(seenWorkspaceId)).toBe("proj-1")
|
|
462
|
+
expect(String(seenChatId)).toBe("chat-optimistic-1")
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test("chat.rename triggers onStateChange", async () => {
|
|
466
|
+
let changed = false
|
|
467
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
468
|
+
const res = await sendCommand(clientNc, { type: "chat.rename", chatId: "chat-1", title: "New Title" })
|
|
469
|
+
expect(res.ok).toBe(true)
|
|
470
|
+
expect(changed).toBe(true)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test("chat.delete disposes agent runtime state then deletes", async () => {
|
|
474
|
+
const disposed: string[] = []
|
|
475
|
+
const deleted: string[] = []
|
|
476
|
+
const mockAgent = {
|
|
477
|
+
...createMockAgent(),
|
|
478
|
+
disposeChat: async (id: string) => { disposed.push(id) },
|
|
479
|
+
}
|
|
480
|
+
const mockStore = {
|
|
481
|
+
...createMockStore(),
|
|
482
|
+
deleteChat: async (id: string) => { deleted.push(id) },
|
|
483
|
+
}
|
|
484
|
+
const { clientNc } = await setup({
|
|
485
|
+
agent: mockAgent as never,
|
|
486
|
+
store: mockStore as never,
|
|
487
|
+
})
|
|
488
|
+
const res = await sendCommand(clientNc, { type: "chat.delete", chatId: "chat-99" })
|
|
489
|
+
expect(res.ok).toBe(true)
|
|
490
|
+
expect(disposed).toEqual(["chat-99"])
|
|
491
|
+
expect(deleted).toEqual(["chat-99"])
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
test("chat.send returns agent result", async () => {
|
|
495
|
+
const { clientNc } = await setup()
|
|
496
|
+
const res = await sendCommand(clientNc, {
|
|
497
|
+
type: "chat.send",
|
|
498
|
+
chatId: "chat-1",
|
|
499
|
+
content: "Hello",
|
|
500
|
+
})
|
|
501
|
+
expect(res.ok).toBe(true)
|
|
502
|
+
expect(res.result).toEqual({ chatId: "chat-1" })
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
test("chat.queue returns agent queue result", async () => {
|
|
506
|
+
const queued: unknown[] = []
|
|
507
|
+
const mockAgent = {
|
|
508
|
+
...createMockAgent(),
|
|
509
|
+
queue: async (cmd: unknown) => {
|
|
510
|
+
queued.push(cmd)
|
|
511
|
+
return { chatId: "chat-1", queued: true }
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
515
|
+
const res = await sendCommand(clientNc, {
|
|
516
|
+
type: "chat.queue",
|
|
517
|
+
chatId: "chat-1",
|
|
518
|
+
content: "Follow-up",
|
|
519
|
+
})
|
|
520
|
+
expect(res.ok).toBe(true)
|
|
521
|
+
expect(res.result).toEqual({ chatId: "chat-1", queued: true })
|
|
522
|
+
expect(queued).toEqual([{
|
|
523
|
+
type: "chat.queue",
|
|
524
|
+
chatId: "chat-1",
|
|
525
|
+
content: "Follow-up",
|
|
526
|
+
}])
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
test("chat.cancel calls agent.cancel", async () => {
|
|
530
|
+
const cancelled: string[] = []
|
|
531
|
+
const mockAgent = {
|
|
532
|
+
...createMockAgent(),
|
|
533
|
+
cancel: async (id: string) => { cancelled.push(id) },
|
|
534
|
+
}
|
|
535
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
536
|
+
const res = await sendCommand(clientNc, { type: "chat.cancel", chatId: "chat-5" })
|
|
537
|
+
expect(res.ok).toBe(true)
|
|
538
|
+
expect(cancelled).toEqual(["chat-5"])
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
test("chat.respondTool calls agent.respondTool", async () => {
|
|
542
|
+
const responses: unknown[] = []
|
|
543
|
+
const mockAgent = {
|
|
544
|
+
...createMockAgent(),
|
|
545
|
+
respondTool: async (cmd: unknown) => { responses.push(cmd) },
|
|
546
|
+
}
|
|
547
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
548
|
+
const res = await sendCommand(clientNc, {
|
|
549
|
+
type: "chat.respondTool",
|
|
550
|
+
chatId: "chat-1",
|
|
551
|
+
toolUseId: "tool-1",
|
|
552
|
+
result: { accepted: true },
|
|
553
|
+
})
|
|
554
|
+
expect(res.ok).toBe(true)
|
|
555
|
+
expect(responses).toHaveLength(1)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
// ── PR5: chat.selectRunner ────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
test("chat.selectRunner calls store.setChatRunner and returns ok", async () => {
|
|
561
|
+
const calls: Array<{ chatId: string; runnerId: string | null }> = []
|
|
562
|
+
const mockStore = {
|
|
563
|
+
...createMockStore(),
|
|
564
|
+
setChatRunner: async (chatId: string, runnerId: string | null) => {
|
|
565
|
+
calls.push({ chatId, runnerId })
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
const { clientNc } = await setup({ store: mockStore as never })
|
|
569
|
+
const res = await sendCommand(clientNc, {
|
|
570
|
+
type: "chat.selectRunner",
|
|
571
|
+
chatId: "chat-1",
|
|
572
|
+
runnerId: "runner-abc",
|
|
573
|
+
})
|
|
574
|
+
expect(res.ok).toBe(true)
|
|
575
|
+
expect(calls).toEqual([{ chatId: "chat-1", runnerId: "runner-abc" }])
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
test("chat.send maps RunnerPickRequired to needsPick result", async () => {
|
|
579
|
+
const { RunnerPickRequired } = await import("./runner-proxy")
|
|
580
|
+
const mockAgent = {
|
|
581
|
+
...createMockAgent(),
|
|
582
|
+
send: async () => {
|
|
583
|
+
throw new RunnerPickRequired({
|
|
584
|
+
chatId: "chat-pick",
|
|
585
|
+
candidates: [
|
|
586
|
+
{ runnerId: "runner-A", state: "online", capabilities: null, isShared: true, incompatible: false, protocolVersion: 1, lastSeenAt: null, pid: null, ownerId: null },
|
|
587
|
+
],
|
|
588
|
+
reason: "ambiguous",
|
|
589
|
+
})
|
|
590
|
+
},
|
|
591
|
+
}
|
|
592
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
593
|
+
const res = await sendCommand(clientNc, {
|
|
594
|
+
type: "chat.send",
|
|
595
|
+
chatId: "chat-pick",
|
|
596
|
+
content: "Hello",
|
|
597
|
+
})
|
|
598
|
+
// Must be ok:true with needsPick structure (not ok:false)
|
|
599
|
+
expect(res.ok).toBe(true)
|
|
600
|
+
const result = res.result as { needsPick: boolean; chatId: string; candidates: unknown[]; reason: string }
|
|
601
|
+
expect(result.needsPick).toBe(true)
|
|
602
|
+
expect(result.chatId).toBe("chat-pick")
|
|
603
|
+
expect(result.reason).toBe("ambiguous")
|
|
604
|
+
expect(Array.isArray(result.candidates)).toBe(true)
|
|
605
|
+
expect(result.candidates).toHaveLength(1)
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
test("chat.queue maps RunnerPickRequired to needsPick result", async () => {
|
|
609
|
+
const { RunnerPickRequired } = await import("./runner-proxy")
|
|
610
|
+
const mockAgent = {
|
|
611
|
+
...createMockAgent(),
|
|
612
|
+
queue: async () => {
|
|
613
|
+
throw new RunnerPickRequired({
|
|
614
|
+
chatId: "chat-pick",
|
|
615
|
+
candidates: [],
|
|
616
|
+
reason: "sticky_offline",
|
|
617
|
+
})
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
621
|
+
const res = await sendCommand(clientNc, {
|
|
622
|
+
type: "chat.queue",
|
|
623
|
+
chatId: "chat-pick",
|
|
624
|
+
content: "Follow-up",
|
|
625
|
+
})
|
|
626
|
+
expect(res.ok).toBe(true)
|
|
627
|
+
const result = res.result as { needsPick: boolean; reason: string }
|
|
628
|
+
expect(result.needsPick).toBe(true)
|
|
629
|
+
expect(result.reason).toBe("sticky_offline")
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test("chat.send still propagates non-RunnerPickRequired errors as ok:false", async () => {
|
|
633
|
+
const mockAgent = {
|
|
634
|
+
...createMockAgent(),
|
|
635
|
+
send: async () => { throw new Error("runner unavailable") },
|
|
636
|
+
}
|
|
637
|
+
const { clientNc } = await setup({ agent: mockAgent as never })
|
|
638
|
+
const res = await sendCommand(clientNc, {
|
|
639
|
+
type: "chat.send",
|
|
640
|
+
chatId: "chat-1",
|
|
641
|
+
content: "Hello",
|
|
642
|
+
})
|
|
643
|
+
expect(res.ok).toBe(false)
|
|
644
|
+
expect(res.error).toBe("runner unavailable")
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
test("project.open returns workspaceId and triggers onStateChange", async () => {
|
|
650
|
+
let changed = false
|
|
651
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
652
|
+
const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/test-project" })
|
|
653
|
+
expect(res.ok).toBe(true)
|
|
654
|
+
expect(res.result).toEqual({ workspaceId: "proj-1" })
|
|
655
|
+
expect(changed).toBe(true)
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
test("project.create returns workspaceId", async () => {
|
|
659
|
+
const { clientNc } = await setup()
|
|
660
|
+
const res = await sendCommand(clientNc, {
|
|
661
|
+
type: "project.create",
|
|
662
|
+
localPath: "/tmp/test-project",
|
|
663
|
+
title: "Test",
|
|
664
|
+
})
|
|
665
|
+
expect(res.ok).toBe(true)
|
|
666
|
+
expect(res.result).toEqual({ workspaceId: "proj-1" })
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test("project.remove disposes chats and triggers onStateChange", async () => {
|
|
670
|
+
let changed = false
|
|
671
|
+
const disposed: string[] = []
|
|
672
|
+
const mockAgent = {
|
|
673
|
+
...createMockAgent(),
|
|
674
|
+
disposeChat: async (id: string) => { disposed.push(id) },
|
|
675
|
+
}
|
|
676
|
+
const { clientNc } = await setup({
|
|
677
|
+
agent: mockAgent as never,
|
|
678
|
+
onStateChange: () => { changed = true },
|
|
679
|
+
})
|
|
680
|
+
const res = await sendCommand(clientNc, { type: "project.remove", workspaceId: "proj-1" })
|
|
681
|
+
expect(res.ok).toBe(true)
|
|
682
|
+
expect(disposed).toContain("chat-1")
|
|
683
|
+
expect(changed).toBe(true)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
test("terminal.create returns snapshot", async () => {
|
|
687
|
+
const { clientNc } = await setup()
|
|
688
|
+
const res = await sendCommand(clientNc, {
|
|
689
|
+
type: "terminal.create",
|
|
690
|
+
workspaceId: "proj-1",
|
|
691
|
+
terminalId: "term-1",
|
|
692
|
+
cols: 80,
|
|
693
|
+
rows: 24,
|
|
694
|
+
scrollback: 1000,
|
|
695
|
+
})
|
|
696
|
+
expect(res.ok).toBe(true)
|
|
697
|
+
const result = res.result as { terminalId: string }
|
|
698
|
+
expect(result.terminalId).toBe("term-1")
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
test("terminal.create errors for missing project", async () => {
|
|
702
|
+
const { clientNc } = await setup()
|
|
703
|
+
const res = await sendCommand(clientNc, {
|
|
704
|
+
type: "terminal.create",
|
|
705
|
+
workspaceId: "nonexistent",
|
|
706
|
+
terminalId: "term-1",
|
|
707
|
+
cols: 80,
|
|
708
|
+
rows: 24,
|
|
709
|
+
scrollback: 1000,
|
|
710
|
+
})
|
|
711
|
+
expect(res.ok).toBe(false)
|
|
712
|
+
expect(res.error).toBe("Project not found")
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
test("terminal.input does not trigger onStateChange", async () => {
|
|
716
|
+
let changed = false
|
|
717
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
718
|
+
const res = await sendCommand(clientNc, { type: "terminal.input", terminalId: "term-1", data: "ls\n" })
|
|
719
|
+
expect(res.ok).toBe(true)
|
|
720
|
+
expect(changed).toBe(false)
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
test("terminal.resize does not trigger onStateChange", async () => {
|
|
724
|
+
let changed = false
|
|
725
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
726
|
+
const res = await sendCommand(clientNc, { type: "terminal.resize", terminalId: "term-1", cols: 120, rows: 40 })
|
|
727
|
+
expect(res.ok).toBe(true)
|
|
728
|
+
expect(changed).toBe(false)
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test("terminal.close triggers onStateChange (publishes null snapshot)", async () => {
|
|
732
|
+
let changed = false
|
|
733
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
734
|
+
const res = await sendCommand(clientNc, { type: "terminal.close", terminalId: "term-1" })
|
|
735
|
+
expect(res.ok).toBe(true)
|
|
736
|
+
expect(changed).toBe(true)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test("invalid JSON payload returns error", async () => {
|
|
740
|
+
const { clientNc } = await setup()
|
|
741
|
+
const msg = await clientNc.request(
|
|
742
|
+
commandSubject("system.ping"),
|
|
743
|
+
encoder.encode("not json{{{"),
|
|
744
|
+
{ timeout: 2000 },
|
|
745
|
+
)
|
|
746
|
+
const res = decode<CommandResponse>(msg.data)
|
|
747
|
+
expect(res.ok).toBe(false)
|
|
748
|
+
expect(res.error).toBe("Invalid JSON payload")
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
test("missing command type returns error", async () => {
|
|
752
|
+
const { clientNc } = await setup()
|
|
753
|
+
const msg = await clientNc.request(
|
|
754
|
+
commandSubject("system.ping"),
|
|
755
|
+
encode({ foo: "bar" }),
|
|
756
|
+
{ timeout: 2000 },
|
|
757
|
+
)
|
|
758
|
+
const res = decode<CommandResponse>(msg.data)
|
|
759
|
+
expect(res.ok).toBe(false)
|
|
760
|
+
expect(res.error).toBe("Missing command type")
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
test("command handler error returns ok:false with message", async () => {
|
|
764
|
+
const mockStore = {
|
|
765
|
+
...createMockStore(),
|
|
766
|
+
openProject: async () => { throw new Error("Disk full") },
|
|
767
|
+
}
|
|
768
|
+
const { clientNc } = await setup({ store: mockStore as never })
|
|
769
|
+
const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/fail" })
|
|
770
|
+
expect(res.ok).toBe(false)
|
|
771
|
+
expect(res.error).toBe("Disk full")
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
test("non-Error throw returns stringified message", async () => {
|
|
775
|
+
const mockStore = {
|
|
776
|
+
...createMockStore(),
|
|
777
|
+
openProject: async () => { throw "string error" },
|
|
778
|
+
}
|
|
779
|
+
const { clientNc } = await setup({ store: mockStore as never })
|
|
780
|
+
const res = await sendCommand(clientNc, { type: "project.open", localPath: "/tmp/fail" })
|
|
781
|
+
expect(res.ok).toBe(false)
|
|
782
|
+
expect(res.error).toBe("string error")
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
test("dispose unsubscribes from commands", async () => {
|
|
786
|
+
const { clientNc } = await setup()
|
|
787
|
+
|
|
788
|
+
// Verify it works before dispose
|
|
789
|
+
const before = await sendCommand(clientNc, { type: "system.ping" })
|
|
790
|
+
expect(before.ok).toBe(true)
|
|
791
|
+
|
|
792
|
+
// Dispose and verify requests timeout
|
|
793
|
+
disposeFn?.()
|
|
794
|
+
disposeFn = null
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
await clientNc.request(commandSubject("system.ping"), encode({ type: "system.ping" }), { timeout: 300 })
|
|
798
|
+
expect(true).toBe(false) // should not reach
|
|
799
|
+
} catch (error) {
|
|
800
|
+
expect(error).toBeDefined()
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
test("multiple commands in sequence", async () => {
|
|
805
|
+
let changeCount = 0
|
|
806
|
+
const { clientNc } = await setup({ onStateChange: () => { changeCount++ } })
|
|
807
|
+
|
|
808
|
+
const ping = await sendCommand(clientNc, { type: "system.ping" })
|
|
809
|
+
expect(ping.ok).toBe(true)
|
|
810
|
+
|
|
811
|
+
const create = await sendCommand(clientNc, { type: "chat.create", workspaceId: "proj-1" })
|
|
812
|
+
expect(create.ok).toBe(true)
|
|
813
|
+
|
|
814
|
+
const read = await sendCommand(clientNc, { type: "update.check" })
|
|
815
|
+
expect(read.ok).toBe(true)
|
|
816
|
+
|
|
817
|
+
// Only chat.create should have triggered onStateChange
|
|
818
|
+
expect(changeCount).toBe(1)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
test("snapshot.subscribe registers subscription and returns snapshot", async () => {
|
|
822
|
+
let addCalled = false
|
|
823
|
+
let addedId = ""
|
|
824
|
+
const mockPublisher = {
|
|
825
|
+
addSubscription: (id: string) => { addCalled = true; addedId = id },
|
|
826
|
+
removeSubscription: () => {},
|
|
827
|
+
getSnapshot: async () => ({ mock: "snapshot" }),
|
|
828
|
+
broadcastSnapshots: async () => {},
|
|
829
|
+
publishChatMessage: () => {},
|
|
830
|
+
refreshSessions: async () => {},
|
|
831
|
+
dispose: () => {},
|
|
832
|
+
}
|
|
833
|
+
const { clientNc } = await setup({ publisher: mockPublisher })
|
|
834
|
+
const res = await sendCommand(clientNc, {
|
|
835
|
+
type: "snapshot.subscribe",
|
|
836
|
+
subscriptionId: "sub-1",
|
|
837
|
+
topic: { type: "sidebar" },
|
|
838
|
+
})
|
|
839
|
+
expect(res.ok).toBe(true)
|
|
840
|
+
expect(res.result).toEqual({ mock: "snapshot" })
|
|
841
|
+
expect(addCalled).toBe(true)
|
|
842
|
+
expect(addedId).toBe("sub-1")
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
test("chat.getMessageCount returns the persisted transcript length", async () => {
|
|
846
|
+
const { clientNc } = await setup({
|
|
847
|
+
store: {
|
|
848
|
+
...createMockStore(),
|
|
849
|
+
getMessageCount: async (chatId: string) => (chatId === "chat-1" ? 3 : 0),
|
|
850
|
+
} as never,
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
const res = await sendCommand(clientNc, {
|
|
854
|
+
type: "chat.getMessageCount",
|
|
855
|
+
chatId: "chat-1",
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
expect(res.ok).toBe(true)
|
|
859
|
+
expect(res.result).toEqual({ messageCount: 3 })
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
test("chat.getRenderUnits returns folded transcript render units", async () => {
|
|
863
|
+
const { clientNc } = await setup({
|
|
864
|
+
store: {
|
|
865
|
+
...createMockStore(),
|
|
866
|
+
getMessages: async () => [
|
|
867
|
+
{ _id: "e1", kind: "assistant_text", createdAt: 1, text: "Checking" },
|
|
868
|
+
{
|
|
869
|
+
_id: "e2",
|
|
870
|
+
kind: "tool_call",
|
|
871
|
+
createdAt: 2,
|
|
872
|
+
tool: {
|
|
873
|
+
kind: "tool",
|
|
874
|
+
toolKind: "bash",
|
|
875
|
+
toolName: "Bash",
|
|
876
|
+
toolId: "tool-1",
|
|
877
|
+
input: { command: "pwd" },
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
{ _id: "e3", kind: "assistant_text", createdAt: 3, text: "Done" },
|
|
881
|
+
],
|
|
882
|
+
} as never,
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
const res = await sendCommand(clientNc, {
|
|
886
|
+
type: "chat.getRenderUnits",
|
|
887
|
+
chatId: "chat-1",
|
|
888
|
+
offset: 0,
|
|
889
|
+
limit: 3,
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
expect(res.ok).toBe(true)
|
|
893
|
+
expect((res.result as Array<{ id: string; kind: string }>).map((unit) => [unit.id, unit.kind])).toEqual([
|
|
894
|
+
["wip:e1:e2", "wip_block"],
|
|
895
|
+
["assistant_response:e3", "assistant_response"],
|
|
896
|
+
])
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
test("chat.getExternalSessionMessages returns transcript entries for a provider session", async () => {
|
|
900
|
+
const sessionId = `responders-codex-${Date.now()}`
|
|
901
|
+
const codexSessionsDir = path.join(process.env.HOME ?? "", ".codex", "sessions")
|
|
902
|
+
await mkdir(codexSessionsDir, { recursive: true })
|
|
903
|
+
await writeFile(path.join(codexSessionsDir, `${sessionId}.jsonl`), [
|
|
904
|
+
JSON.stringify({ type: "session_meta", payload: { id: sessionId, cwd: "/tmp/test-project" } }),
|
|
905
|
+
JSON.stringify({ timestamp: "2026-04-13T12:00:00.000Z", type: "event_msg", payload: { type: "agent_message", message: "Loaded external transcript" } }),
|
|
906
|
+
].join("\n") + "\n")
|
|
907
|
+
|
|
908
|
+
const { clientNc } = await setup()
|
|
909
|
+
const res = await sendCommand(clientNc, {
|
|
910
|
+
type: "chat.getExternalSessionMessages",
|
|
911
|
+
parentChatId: "chat-1",
|
|
912
|
+
sessionId,
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
expect(res.ok).toBe(true)
|
|
916
|
+
expect(res.result).toEqual([
|
|
917
|
+
expect.objectContaining({ kind: "assistant_text", text: "Loaded external transcript" }),
|
|
918
|
+
])
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
test("snapshot.unsubscribe removes subscription", async () => {
|
|
922
|
+
let removedId = ""
|
|
923
|
+
const mockPublisher = {
|
|
924
|
+
addSubscription: () => {},
|
|
925
|
+
removeSubscription: (id: string) => { removedId = id },
|
|
926
|
+
getSnapshot: async () => null,
|
|
927
|
+
broadcastSnapshots: async () => {},
|
|
928
|
+
publishChatMessage: () => {},
|
|
929
|
+
refreshSessions: async () => {},
|
|
930
|
+
dispose: () => {},
|
|
931
|
+
}
|
|
932
|
+
const { clientNc } = await setup({ publisher: mockPublisher })
|
|
933
|
+
const res = await sendCommand(clientNc, {
|
|
934
|
+
type: "snapshot.unsubscribe",
|
|
935
|
+
subscriptionId: "sub-1",
|
|
936
|
+
})
|
|
937
|
+
expect(res.ok).toBe(true)
|
|
938
|
+
expect(removedId).toBe("sub-1")
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
test("snapshot.subscribe does not trigger onStateChange", async () => {
|
|
942
|
+
let changed = false
|
|
943
|
+
const { clientNc } = await setup({ onStateChange: () => { changed = true } })
|
|
944
|
+
await sendCommand(clientNc, {
|
|
945
|
+
type: "snapshot.subscribe",
|
|
946
|
+
subscriptionId: "sub-1",
|
|
947
|
+
topic: { type: "sidebar" },
|
|
948
|
+
})
|
|
949
|
+
expect(changed).toBe(false)
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
test("snapshot.subscribe with local-projects triggers refresh", async () => {
|
|
953
|
+
let refreshed = false
|
|
954
|
+
const { clientNc } = await setup({
|
|
955
|
+
refreshDiscovery: async () => { refreshed = true; return [] },
|
|
956
|
+
})
|
|
957
|
+
await sendCommand(clientNc, {
|
|
958
|
+
type: "snapshot.subscribe",
|
|
959
|
+
subscriptionId: "sub-lp",
|
|
960
|
+
topic: { type: "local-workspaces" },
|
|
961
|
+
})
|
|
962
|
+
expect(refreshed).toBe(true)
|
|
963
|
+
})
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
describe("nats-responders runner team (US-RTN)", () => {
|
|
967
|
+
// Uses a real EventStore so the command → store → state round-trip is exercised.
|
|
968
|
+
async function setupWithStore() {
|
|
969
|
+
const dir = await mkdtemp(path.join(tmpdir(), "kanna-rt-resp-"))
|
|
970
|
+
tempDir = dir
|
|
971
|
+
const store = new EventStore(dir)
|
|
972
|
+
await store.initialize()
|
|
973
|
+
let stateChanges = 0
|
|
974
|
+
const { clientNc } = await setup({
|
|
975
|
+
store: store as never,
|
|
976
|
+
onStateChange: () => { stateChanges += 1 },
|
|
977
|
+
})
|
|
978
|
+
return { clientNc, store, stateChanges: () => stateChanges }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
test("team.member.save persists and team.member.list returns it", async () => {
|
|
982
|
+
const { clientNc, store } = await setupWithStore()
|
|
983
|
+
const save = await sendCommand(clientNc, { type: "team.member.save", member: { id: "m1", name: "Alice" } })
|
|
984
|
+
expect(save.ok).toBe(true)
|
|
985
|
+
expect(store.state.teamMembers.get("m1")?.name).toBe("Alice")
|
|
986
|
+
|
|
987
|
+
const list = await sendCommand(clientNc, { type: "team.member.list" })
|
|
988
|
+
expect(list.ok).toBe(true)
|
|
989
|
+
expect((list.result as { members: { id: string; name: string }[] }).members).toEqual([{ id: "m1", name: "Alice" }])
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
test("runner.label.set persists name + assignment and triggers a state change", async () => {
|
|
993
|
+
const { clientNc, store, stateChanges } = await setupWithStore()
|
|
994
|
+
const before = stateChanges()
|
|
995
|
+
const res = await sendCommand(clientNc, { type: "runner.label.set", runnerId: "runner-1", name: "Studio Mac", memberId: "m1" })
|
|
996
|
+
expect(res.ok).toBe(true)
|
|
997
|
+
expect(store.state.runnerLabels.get("runner-1")?.name).toBe("Studio Mac")
|
|
998
|
+
expect(store.state.runnerLabels.get("runner-1")?.memberId).toBe("m1")
|
|
999
|
+
// Mutating command must republish snapshots.
|
|
1000
|
+
expect(stateChanges()).toBe(before + 1)
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
test("team.member.remove unassigns runners", async () => {
|
|
1004
|
+
const { clientNc, store } = await setupWithStore()
|
|
1005
|
+
await sendCommand(clientNc, { type: "team.member.save", member: { id: "m1", name: "Alice" } })
|
|
1006
|
+
await sendCommand(clientNc, { type: "runner.label.set", runnerId: "runner-1", name: "Box", memberId: "m1" })
|
|
1007
|
+
await sendCommand(clientNc, { type: "team.member.remove", memberId: "m1" })
|
|
1008
|
+
expect(store.state.teamMembers.get("m1")).toBeUndefined()
|
|
1009
|
+
expect(store.state.runnerLabels.get("runner-1")?.memberId).toBeNull()
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
test("team.member.list is non-mutating (no state change)", async () => {
|
|
1013
|
+
const { clientNc, stateChanges } = await setupWithStore()
|
|
1014
|
+
const before = stateChanges()
|
|
1015
|
+
await sendCommand(clientNc, { type: "team.member.list" })
|
|
1016
|
+
expect(stateChanges()).toBe(before)
|
|
1017
|
+
})
|
|
1018
|
+
})
|