@dsdenes/devboard 0.1.560 → 0.1.563
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/AGENTS.md +59 -0
- package/README.md +2497 -26
- package/bin/devboard.mjs +352 -118
- package/client/App.svelte +523 -0
- package/client/FeedbackOverlay.svelte +1125 -0
- package/client/FlowMode.svelte +1015 -0
- package/client/app.css +3922 -0
- package/client/env.d.ts +23 -0
- package/client/lib/commands.ts +836 -0
- package/client/lib/components/ActivityBar.svelte +295 -0
- package/client/lib/components/AdrTable.svelte +241 -0
- package/client/lib/components/CommandPalette.svelte +198 -0
- package/client/lib/components/ConceptKind.svelte +66 -0
- package/client/lib/components/ConfirmDialog.svelte +62 -0
- package/client/lib/components/DataTable.svelte +243 -0
- package/client/lib/components/DetailSplit.svelte +68 -0
- package/client/lib/components/DiscoverOverview.svelte +898 -0
- package/client/lib/components/DiscoverWorkspace.svelte +437 -0
- package/client/lib/components/DraftsPage.svelte +186 -0
- package/client/lib/components/GoalWorkspace.svelte +1042 -0
- package/client/lib/components/IssuesAgentFocus.svelte +117 -0
- package/client/lib/components/IssuesAgentLog.svelte +50 -0
- package/client/lib/components/IssuesQuestion.svelte +95 -0
- package/client/lib/components/IssuesWorkspace.svelte +577 -0
- package/client/lib/components/MermaidDiagram.svelte +89 -0
- package/client/lib/components/MonacoEditor.svelte +190 -0
- package/client/lib/components/NotificationRegion.svelte +195 -0
- package/client/lib/components/PaneHandle.svelte +25 -0
- package/client/lib/components/ScoutIssues.svelte +215 -0
- package/client/lib/components/ScoutWorkspace.svelte +180 -0
- package/client/lib/components/Sidebar.svelte +622 -0
- package/client/lib/components/StatusBar.svelte +118 -0
- package/client/lib/components/Terminal.svelte +193 -0
- package/client/lib/components/WatchdogWorkspace.svelte +537 -0
- package/client/lib/components/ui/button.svelte +56 -0
- package/client/lib/components/ui/icon-button.svelte +56 -0
- package/client/lib/components/ui/input.svelte +19 -0
- package/client/lib/components/ui/page-header.svelte +120 -0
- package/client/lib/components/ui/stage-track.svelte +141 -0
- package/client/lib/components/watchdog-indicator.svelte +189 -0
- package/client/lib/connection.svelte.ts +210 -0
- package/client/lib/dependency-diagram.test.ts +90 -0
- package/client/lib/dependency-diagram.ts +75 -0
- package/client/lib/discover-format.test.ts +265 -0
- package/client/lib/discover-format.ts +394 -0
- package/client/lib/discover.svelte.ts +412 -0
- package/client/lib/draft-format.test.ts +48 -0
- package/client/lib/draft-format.ts +13 -0
- package/client/lib/flow-save.test.ts +148 -0
- package/client/lib/flow.svelte.ts +445 -0
- package/client/lib/goal-reconnect.test.ts +282 -0
- package/client/lib/goal.svelte.ts +652 -0
- package/client/lib/issues.svelte.ts +439 -0
- package/client/lib/layout.svelte.ts +234 -0
- package/client/lib/notification-issue.test.ts +53 -0
- package/client/lib/notification-issue.ts +27 -0
- package/client/lib/notifications.svelte.ts +62 -0
- package/client/lib/poll-status.test.ts +141 -0
- package/client/lib/poll-status.ts +88 -0
- package/client/lib/record-status.test.ts +19 -0
- package/client/lib/record-status.ts +25 -0
- package/client/lib/scout.svelte.ts +164 -0
- package/client/lib/skill-document.test.ts +77 -0
- package/client/lib/skill-document.ts +57 -0
- package/client/lib/status-tone.test.ts +40 -0
- package/client/lib/status-tone.ts +33 -0
- package/client/lib/types.ts +578 -0
- package/client/lib/utils.ts +6 -0
- package/client/lib/viewport.svelte.ts +20 -0
- package/client/lib/watchdog.svelte.ts +69 -0
- package/client/main.ts +7 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
- package/dist/assets/arc-BZgCSX2H.js +1 -0
- package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
- package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
- package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
- package/dist/assets/channel-CgM4lcUW.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
- package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
- package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
- package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
- package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
- package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
- package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
- package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
- package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
- package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
- package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
- package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
- package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
- package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
- package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
- package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-D7JM2Yhv.js +1 -0
- package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
- package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
- package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
- package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
- package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
- package/dist/assets/dist-Dce-2YbA.js +1 -0
- package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
- package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
- package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
- package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
- package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
- package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
- package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
- package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-ByQ-XX38.css +1 -0
- package/dist/assets/index-CIf5TpfY.js +214 -0
- package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
- package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
- package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
- package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
- package/dist/assets/katex-DJhrJSbw.js +257 -0
- package/dist/assets/line-N9XiCREw.js +1 -0
- package/dist/assets/linear-C9hnqmJv.js +1 -0
- package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
- package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
- package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
- package/dist/assets/monaco-DjpsE8to.css +1 -0
- package/dist/assets/monaco-DqzJaNIe.js +908 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
- package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
- package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
- package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
- package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
- package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
- package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
- package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
- package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
- package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
- package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-CGp3bfio.js +1 -0
- package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
- package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
- package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
- package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
- package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
- package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
- package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
- package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
- package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/assets/xterm-C8zLTNGk.js +16 -0
- package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +6 -0
- package/dist/index.html +34 -0
- package/extensions/implement-goal-issue.ts +368 -0
- package/extensions/implement-next-epic.ts +835 -0
- package/index.html +28 -0
- package/install.sh +111 -0
- package/issues/control.ts +91 -0
- package/issues/guards.ts +12 -0
- package/issues/ledger.ts +122 -0
- package/issues/legacy.ts +68 -0
- package/issues/lock.ts +305 -0
- package/issues/paths.ts +76 -0
- package/issues/preflight.ts +189 -0
- package/issues/repository.ts +203 -0
- package/issues/scout/actions.ts +255 -0
- package/issues/scout/control.ts +80 -0
- package/issues/scout/github.ts +358 -0
- package/issues/scout/paths.ts +129 -0
- package/issues/scout/preflight.ts +277 -0
- package/issues/scout/recommendation.ts +400 -0
- package/issues/scout/scan.ts +143 -0
- package/issues/scout/state.ts +448 -0
- package/issues/scout/worker.ts +1282 -0
- package/issues/state.ts +584 -0
- package/issues/tracker.ts +271 -0
- package/issues/worker.ts +1065 -0
- package/lib/dependencies.ts +61 -0
- package/lib/forge-auth.ts +953 -0
- package/lib/forge.ts +141 -0
- package/lib/gitea.ts +729 -0
- package/lib/github-release.ts +318 -0
- package/lib/inventory.test.ts +79 -0
- package/lib/inventory.ts +53 -0
- package/lib/release.test.ts +269 -0
- package/lib/release.ts +801 -0
- package/omp/commands/adr-resolve.md +8 -0
- package/omp/commands/discover.md +6 -0
- package/omp/commands/epic-next.md +8 -0
- package/omp/commands/flow-change.md +8 -0
- package/omp/commands/flow-goal.md +8 -0
- package/omp/commands/flow-implement.md +8 -0
- package/omp/commands/flow-widget.md +8 -0
- package/omp/commands/goal-draft.md +8 -0
- package/omp/commands/goal-outcome.md +8 -0
- package/omp/commands/implement-issue.md +8 -0
- package/omp/commands/prepare-skills.md +6 -0
- package/omp/commands/scout-recommendations.md +8 -0
- package/omp/commands/setup-devboard.md +24 -0
- package/omp/config.yml +12 -0
- package/omp/extensions/implement-goal-issue.ts +2 -0
- package/omp/extensions/implement-next-epic.ts +2 -0
- package/omp/mcp.json +9 -0
- package/package.json +86 -7
- package/prompts/adr-resolve.md +130 -0
- package/prompts/discover.md +541 -0
- package/prompts/epic-next.md +533 -0
- package/prompts/flow-change.md +76 -0
- package/prompts/flow-goal.md +126 -0
- package/prompts/flow-implement.md +103 -0
- package/prompts/flow-widget.md +54 -0
- package/prompts/goal-draft.md +147 -0
- package/prompts/goal-outcome.md +87 -0
- package/prompts/implement-issue.md +66 -0
- package/prompts/prepare-skills.md +142 -0
- package/prompts/scout-recommendations.md +143 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +6 -0
- package/server/acp.ts +540 -0
- package/server/activity.test.ts +96 -0
- package/server/activity.ts +48 -0
- package/server/app.test.ts +882 -0
- package/server/app.ts +6179 -0
- package/server/ask.test.ts +543 -0
- package/server/ask.ts +589 -0
- package/server/dependencies.test.ts +118 -0
- package/server/discover-remote.test.ts +1602 -0
- package/server/discover-sheet.test.ts +176 -0
- package/server/discover.test.ts +1510 -0
- package/server/discover.ts +3198 -0
- package/server/feedback.test.ts +458 -0
- package/server/feedback.ts +369 -0
- package/server/fixtures/adr-source.ts +71 -0
- package/server/fixtures/hang-download.ts +14 -0
- package/server/fixtures/stub-agent.ts +1870 -0
- package/server/fixtures/stub-forge.ts +141 -0
- package/server/fixtures/stub-gh.ts +57 -0
- package/server/flow-pr.test.ts +976 -0
- package/server/flow.test.ts +1632 -0
- package/server/flow.ts +2655 -0
- package/server/forge-pr.ts +387 -0
- package/server/goal-creation.test.ts +957 -0
- package/server/goal-draft.test.ts +348 -0
- package/server/goal-drafts.ts +449 -0
- package/server/goal-proposal.test.ts +479 -0
- package/server/goal-proposal.ts +232 -0
- package/server/goal-revision.test.ts +613 -0
- package/server/goal-slug.test.ts +56 -0
- package/server/goal-slug.ts +96 -0
- package/server/goal.ts +20 -0
- package/server/index.ts +147 -0
- package/server/issue-close.test.ts +100 -0
- package/server/issue-close.ts +178 -0
- package/server/issues.test.ts +167 -0
- package/server/issues.ts +479 -0
- package/server/journal.test.ts +350 -0
- package/server/journal.ts +329 -0
- package/server/lifecycle-pause.test.ts +1795 -0
- package/server/lifecycle.test.ts +1879 -0
- package/server/parallel-lifecycle.test.ts +1029 -0
- package/server/persistence-restart.test.ts +723 -0
- package/server/persistence.test.ts +1935 -0
- package/server/records.test.ts +180 -0
- package/server/records.ts +99 -0
- package/server/reset-implementation.test.ts +997 -0
- package/server/run.test.ts +611 -0
- package/server/scout.test.ts +877 -0
- package/server/scout.ts +459 -0
- package/server/skills-preparation.test.ts +1073 -0
- package/server/state.ts +249 -0
- package/server/tracker.ts +369 -0
- package/server/updates.test.ts +444 -0
- package/server/updates.ts +198 -0
- package/server/version.test.ts +140 -0
- package/server/version.ts +151 -0
- package/server/watchdog.ts +389 -0
- package/svelte.config.js +5 -0
- package/templates/host-AGENTS.md +235 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +21 -0
- package/watchdog/cleanup-main.ts +98 -0
- package/watchdog/cleanup.ts +301 -0
- package/watchdog/collect.ts +311 -0
- package/watchdog/coordinator.ts +397 -0
- package/watchdog/delivery.ts +107 -0
- package/watchdog/discovery.ts +209 -0
- package/watchdog/findings.ts +211 -0
- package/watchdog/github.ts +189 -0
- package/watchdog/history.ts +138 -0
- package/watchdog/install.ts +103 -0
- package/watchdog/ledger.ts +188 -0
- package/watchdog/lock.ts +63 -0
- package/watchdog/main.ts +106 -0
- package/watchdog/status.ts +71 -0
- package/watchdog/systemd.ts +96 -0
- package/watchdog/uninstall.ts +30 -0
- package/watchdog/units/devboard-watchdog.service +9 -0
- package/watchdog/units/devboard-watchdog.timer +10 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { Server } from "bun";
|
|
6
|
+
import { createApp } from "./app";
|
|
7
|
+
|
|
8
|
+
const temporaryDirectories: string[] = [];
|
|
9
|
+
const temporaryRoot = join(import.meta.dir, "..", ".tmp");
|
|
10
|
+
const STUB_AGENT = join(import.meta.dir, "fixtures", "stub-agent.ts");
|
|
11
|
+
|
|
12
|
+
const READ_ONLY = "This conversation is read-only. Never edit files, run mutating commands, file issues, commit, or push.";
|
|
13
|
+
const BREVITY = "Answer in at most three short sentences and about 400 characters. No preamble and no restating the question.";
|
|
14
|
+
const PLAIN = "Reply in plain text without Markdown.";
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await Promise.all(
|
|
18
|
+
temporaryDirectories.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function temporaryDirectory(): Promise<string> {
|
|
23
|
+
await mkdir(temporaryRoot, { recursive: true });
|
|
24
|
+
const directory = await mkdtemp(join(temporaryRoot, "ask-"));
|
|
25
|
+
temporaryDirectories.push(directory);
|
|
26
|
+
return directory;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TestServer {
|
|
30
|
+
server: Server<unknown>;
|
|
31
|
+
root: string;
|
|
32
|
+
url: string;
|
|
33
|
+
logPath: string;
|
|
34
|
+
recordPath: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ServerOptions {
|
|
38
|
+
/** Replaces the stub command entirely; a value that cannot start proves failure. */
|
|
39
|
+
command?: string;
|
|
40
|
+
/** Extra stub arguments after `--ask --log=<path>`. */
|
|
41
|
+
agentArguments?: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function startServer(options: ServerOptions = {}): Promise<TestServer> {
|
|
45
|
+
const root = await temporaryDirectory();
|
|
46
|
+
const adrDir = join(root, "adrs");
|
|
47
|
+
const staticDir = join(root, "static");
|
|
48
|
+
const logPath = join(root, "agent.log");
|
|
49
|
+
const recordPath = join(root, "gh-call.json");
|
|
50
|
+
await mkdir(adrDir, { recursive: true });
|
|
51
|
+
await mkdir(staticDir, { recursive: true });
|
|
52
|
+
await writeFile(join(staticDir, "index.html"), "<!doctype html><title>Ask</title>");
|
|
53
|
+
|
|
54
|
+
const command =
|
|
55
|
+
options.command ??
|
|
56
|
+
[`bun ${STUB_AGENT}`, "--ask", `--log=${logPath}`, ...(options.agentArguments ?? [])]
|
|
57
|
+
.filter((part) => part.length > 0)
|
|
58
|
+
.join(" ");
|
|
59
|
+
|
|
60
|
+
const server = createApp({
|
|
61
|
+
adrDir,
|
|
62
|
+
staticDir,
|
|
63
|
+
hostname: "127.0.0.1",
|
|
64
|
+
port: 0,
|
|
65
|
+
repoDir: root,
|
|
66
|
+
stateDir: join(root, "state"),
|
|
67
|
+
agentCommand: command,
|
|
68
|
+
tracker: async () => undefined,
|
|
69
|
+
trackerSettle: { attempts: 1 },
|
|
70
|
+
});
|
|
71
|
+
return { server, root, url: `127.0.0.1:${server.port}`, logPath, recordPath };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function askContext(overrides: Record<string, string> = {}): Record<string, string> {
|
|
75
|
+
return {
|
|
76
|
+
route: "/boards/42",
|
|
77
|
+
url: "http://127.0.0.1:8080/boards/42",
|
|
78
|
+
selector: "footer > span.status-item:nth-of-type(2)",
|
|
79
|
+
viewport: "1440x900 @1x",
|
|
80
|
+
captured_at: "2026-10-27T10:00:00.000Z",
|
|
81
|
+
visible_text: "issues working (3)",
|
|
82
|
+
dom_excerpt: "<span class=\"status-item\" title=\"fixture/host\">issues working (3)</span>",
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface TestClient {
|
|
88
|
+
socket: WebSocket;
|
|
89
|
+
events: Record<string, unknown>[];
|
|
90
|
+
ask(conversationId: string, question: string, context?: Record<string, string>): void;
|
|
91
|
+
closeAsk(conversationId: string): void;
|
|
92
|
+
send(command: unknown): void;
|
|
93
|
+
waitFor(
|
|
94
|
+
predicate: (message: Record<string, unknown>) => boolean,
|
|
95
|
+
timeoutMs?: number,
|
|
96
|
+
): Promise<Record<string, unknown>>;
|
|
97
|
+
close(): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function connect(url: string): Promise<TestClient> {
|
|
101
|
+
const { promise, resolve, reject } = Promise.withResolvers<TestClient>();
|
|
102
|
+
const socket = new WebSocket(`ws://${url}/ws`);
|
|
103
|
+
const events: Record<string, unknown>[] = [];
|
|
104
|
+
const buffered: Record<string, unknown>[] = [];
|
|
105
|
+
const waiters: {
|
|
106
|
+
predicate: (message: Record<string, unknown>) => boolean;
|
|
107
|
+
resolve: (message: Record<string, unknown>) => void;
|
|
108
|
+
}[] = [];
|
|
109
|
+
|
|
110
|
+
socket.onopen = () => {
|
|
111
|
+
resolve({
|
|
112
|
+
socket,
|
|
113
|
+
events,
|
|
114
|
+
send(command) {
|
|
115
|
+
socket.send(JSON.stringify(command));
|
|
116
|
+
},
|
|
117
|
+
ask(conversationId, question, context = askContext()) {
|
|
118
|
+
socket.send(JSON.stringify({ type: "ask", conversationId, question, context }));
|
|
119
|
+
},
|
|
120
|
+
closeAsk(conversationId) {
|
|
121
|
+
socket.send(JSON.stringify({ type: "closeAsk", conversationId }));
|
|
122
|
+
},
|
|
123
|
+
waitFor(predicate, timeoutMs = 10_000) {
|
|
124
|
+
const index = buffered.findIndex(predicate);
|
|
125
|
+
if (index >= 0) {
|
|
126
|
+
const message = buffered.splice(index, 1)[0];
|
|
127
|
+
if (message !== undefined) return Promise.resolve(message);
|
|
128
|
+
}
|
|
129
|
+
const { promise: waiting, resolve: settle } =
|
|
130
|
+
Promise.withResolvers<Record<string, unknown>>();
|
|
131
|
+
waiters.push({ predicate, resolve: settle });
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
const position = waiters.findIndex((waiter) => waiter.resolve === settle);
|
|
134
|
+
if (position >= 0) waiters.splice(position, 1);
|
|
135
|
+
settle({ type: "timeout" });
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
void waiting.finally(() => clearTimeout(timer));
|
|
138
|
+
return waiting;
|
|
139
|
+
},
|
|
140
|
+
close() {
|
|
141
|
+
socket.close();
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
socket.onmessage = (event) => {
|
|
147
|
+
let message: Record<string, unknown>;
|
|
148
|
+
try {
|
|
149
|
+
message = JSON.parse(String(event.data)) as Record<string, unknown>;
|
|
150
|
+
} catch {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
events.push(message);
|
|
154
|
+
const index = waiters.findIndex((waiter) => waiter.predicate(message));
|
|
155
|
+
if (index >= 0) {
|
|
156
|
+
const waiter = waiters.splice(index, 1)[0];
|
|
157
|
+
if (waiter !== undefined) waiter.resolve(message);
|
|
158
|
+
} else {
|
|
159
|
+
buffered.push(message);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
socket.onerror = () => reject(new Error("websocket failed"));
|
|
164
|
+
return promise;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function logLines(logPath: string): string[] {
|
|
168
|
+
if (!existsSync(logPath)) return [];
|
|
169
|
+
return readFileSync(logPath, "utf8").split("\n").filter((line) => line.length > 0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function askLog(logPath: string): {
|
|
173
|
+
spawns: number;
|
|
174
|
+
prompts: string[];
|
|
175
|
+
texts: string[];
|
|
176
|
+
exits: number;
|
|
177
|
+
} {
|
|
178
|
+
const lines = logLines(logPath);
|
|
179
|
+
return {
|
|
180
|
+
spawns: lines.filter((line) => line.startsWith("ask-start ")).length,
|
|
181
|
+
prompts: lines
|
|
182
|
+
.filter((line) => line.startsWith("ask-prompt "))
|
|
183
|
+
.map((line) => line.slice("ask-prompt ".length)),
|
|
184
|
+
texts: lines
|
|
185
|
+
.filter((line) => line.startsWith("ask-text "))
|
|
186
|
+
.map((line) => {
|
|
187
|
+
const encoded = line.slice(line.indexOf(" ", "ask-text ".length) + 1);
|
|
188
|
+
return JSON.parse(encoded) as string;
|
|
189
|
+
}),
|
|
190
|
+
exits: lines.filter((line) => line.startsWith("exit ")).length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function eventually(predicate: () => boolean, timeoutMs = 5_000): Promise<void> {
|
|
195
|
+
const deadline = Date.now() + timeoutMs;
|
|
196
|
+
while (!predicate() && Date.now() < deadline) await Bun.sleep(25);
|
|
197
|
+
expect(predicate()).toBe(true);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function chunks(client: TestClient, conversationId: string): string {
|
|
201
|
+
return client.events
|
|
202
|
+
.filter((event) => event.type === "askChunk" && event.conversationId === conversationId)
|
|
203
|
+
.map((event) => String(event.text ?? ""))
|
|
204
|
+
.join("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
describe("ask conversations", () => {
|
|
208
|
+
test("starts no peer until the first ask, then one peer across two turns", async () => {
|
|
209
|
+
const testServer = await startServer();
|
|
210
|
+
const client = await connect(testServer.url);
|
|
211
|
+
try {
|
|
212
|
+
// Opening the panel is a client-only state; the server sees nothing.
|
|
213
|
+
client.send({ type: "getProject" });
|
|
214
|
+
await client.waitFor((message) => message.type === "project");
|
|
215
|
+
expect(askLog(testServer.logPath).spawns).toBe(0);
|
|
216
|
+
|
|
217
|
+
client.ask("conversation-1", "What does the StatusBar issues label mean?");
|
|
218
|
+
const first = await client.waitFor(
|
|
219
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-1",
|
|
220
|
+
);
|
|
221
|
+
expect(first.type).toBe("askDone");
|
|
222
|
+
const firstAnswer = chunks(client, "conversation-1");
|
|
223
|
+
expect(firstAnswer.startsWith("The issues indicator shows")).toBe(true);
|
|
224
|
+
// Thoughts and tool activity never become answer text.
|
|
225
|
+
expect(firstAnswer.includes("Considering the captured element")).toBe(false);
|
|
226
|
+
expect(firstAnswer.includes("Reading the captured element")).toBe(false);
|
|
227
|
+
expect(
|
|
228
|
+
client.events.filter(
|
|
229
|
+
(event) =>
|
|
230
|
+
event.type === "askChunk" &&
|
|
231
|
+
event.conversationId === "conversation-1" &&
|
|
232
|
+
String(event.text ?? "").length > 0,
|
|
233
|
+
).length,
|
|
234
|
+
).toBeGreaterThanOrEqual(2);
|
|
235
|
+
|
|
236
|
+
client.ask("conversation-1", "Which file renders it?");
|
|
237
|
+
await client.waitFor(
|
|
238
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-1",
|
|
239
|
+
);
|
|
240
|
+
const secondAnswer = chunks(client, "conversation-1").slice(firstAnswer.length);
|
|
241
|
+
expect(secondAnswer.includes("StatusBar.svelte")).toBe(true);
|
|
242
|
+
expect(secondAnswer).not.toBe(firstAnswer);
|
|
243
|
+
|
|
244
|
+
const logged = askLog(testServer.logPath);
|
|
245
|
+
expect(logged.spawns).toBe(1);
|
|
246
|
+
expect(logged.prompts).toHaveLength(2);
|
|
247
|
+
expect(logged.prompts[0]).toContain("What does the StatusBar issues label mean?");
|
|
248
|
+
expect(logged.prompts[0]).not.toContain("Which file renders it?");
|
|
249
|
+
|
|
250
|
+
const context = askContext();
|
|
251
|
+
const [firstPrompt, followUp] = logged.texts;
|
|
252
|
+
expect(firstPrompt).toBeDefined();
|
|
253
|
+
for (const [name, value] of Object.entries(context)) {
|
|
254
|
+
expect(firstPrompt).toContain(`${name}: ${value}`);
|
|
255
|
+
}
|
|
256
|
+
for (const sentence of [READ_ONLY, BREVITY, PLAIN]) {
|
|
257
|
+
expect(firstPrompt).toContain(sentence);
|
|
258
|
+
expect(followUp).toContain(sentence);
|
|
259
|
+
}
|
|
260
|
+
expect(firstPrompt).toContain("Question: What does the StatusBar issues label mean?");
|
|
261
|
+
expect(followUp).toContain("Question: Which file renders it?");
|
|
262
|
+
expect(followUp).not.toContain("Captured element context:");
|
|
263
|
+
expect(existsSync(testServer.recordPath)).toBe(false);
|
|
264
|
+
} finally {
|
|
265
|
+
client.close();
|
|
266
|
+
testServer.server.stop(true);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("refuses a second ask while the first turn is in flight", async () => {
|
|
271
|
+
const testServer = await startServer({ agentArguments: ["--ask-delay-ms=250"] });
|
|
272
|
+
const client = await connect(testServer.url);
|
|
273
|
+
try {
|
|
274
|
+
client.ask("conversation-busy", "What does the status bar show?");
|
|
275
|
+
await client.waitFor(
|
|
276
|
+
(message) => message.type === "askChunk" && message.conversationId === "conversation-busy",
|
|
277
|
+
);
|
|
278
|
+
client.ask("conversation-busy", "And what else?");
|
|
279
|
+
const refused = await client.waitFor(
|
|
280
|
+
(message) =>
|
|
281
|
+
message.type === "askError" && message.conversationId === "conversation-busy",
|
|
282
|
+
);
|
|
283
|
+
expect(String(refused.message)).toContain("still answering");
|
|
284
|
+
await client.waitFor(
|
|
285
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-busy",
|
|
286
|
+
);
|
|
287
|
+
const logged = askLog(testServer.logPath);
|
|
288
|
+
expect(logged.spawns).toBe(1);
|
|
289
|
+
expect(logged.prompts).toHaveLength(1);
|
|
290
|
+
} finally {
|
|
291
|
+
client.close();
|
|
292
|
+
testServer.server.stop(true);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("refuses empty, oversized, and incomplete asks without starting a peer", async () => {
|
|
297
|
+
const testServer = await startServer();
|
|
298
|
+
const client = await connect(testServer.url);
|
|
299
|
+
try {
|
|
300
|
+
client.ask("conversation-bad", " ");
|
|
301
|
+
await client.waitFor(
|
|
302
|
+
(message) =>
|
|
303
|
+
message.type === "askError" && message.conversationId === "conversation-bad",
|
|
304
|
+
);
|
|
305
|
+
client.ask("conversation-bad", "x".repeat(4001));
|
|
306
|
+
const oversized = await client.waitFor(
|
|
307
|
+
(message) =>
|
|
308
|
+
message.type === "askError" && message.conversationId === "conversation-bad",
|
|
309
|
+
);
|
|
310
|
+
expect(String(oversized.message)).toContain("4000");
|
|
311
|
+
|
|
312
|
+
client.socket.send(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
type: "ask",
|
|
315
|
+
conversationId: "conversation-bad",
|
|
316
|
+
question: "Why?",
|
|
317
|
+
context: { route: "/" },
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
const incomplete = await client.waitFor(
|
|
321
|
+
(message) =>
|
|
322
|
+
message.type === "askError" && message.conversationId === "conversation-bad",
|
|
323
|
+
);
|
|
324
|
+
expect(String(incomplete.message).toLowerCase()).toContain("context");
|
|
325
|
+
|
|
326
|
+
const bounded = askContext({ visible_text: "y".repeat(1501) });
|
|
327
|
+
client.ask("conversation-bad", "Why?", bounded);
|
|
328
|
+
await client.waitFor(
|
|
329
|
+
(message) =>
|
|
330
|
+
message.type === "askError" && message.conversationId === "conversation-bad",
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(askLog(testServer.logPath).spawns).toBe(0);
|
|
334
|
+
expect(existsSync(testServer.logPath)).toBe(false);
|
|
335
|
+
} finally {
|
|
336
|
+
client.close();
|
|
337
|
+
testServer.server.stop(true);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("isolates conversations per socket and streams to the asker only", async () => {
|
|
342
|
+
const testServer = await startServer({ agentArguments: ["--ask-delay-ms=60"] });
|
|
343
|
+
const first = await connect(testServer.url);
|
|
344
|
+
const second = await connect(testServer.url);
|
|
345
|
+
try {
|
|
346
|
+
first.ask("conversation-first", "What does the issues label mean?");
|
|
347
|
+
second.ask("conversation-second", "Which file renders it?");
|
|
348
|
+
await first.waitFor(
|
|
349
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-first",
|
|
350
|
+
);
|
|
351
|
+
await second.waitFor(
|
|
352
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-second",
|
|
353
|
+
);
|
|
354
|
+
expect(
|
|
355
|
+
first.events.every((event) => event.conversationId !== "conversation-second"),
|
|
356
|
+
).toBe(true);
|
|
357
|
+
expect(
|
|
358
|
+
second.events.every((event) => event.conversationId !== "conversation-first"),
|
|
359
|
+
).toBe(true);
|
|
360
|
+
expect(askLog(testServer.logPath).spawns).toBe(2);
|
|
361
|
+
expect(askLog(testServer.logPath).prompts).toHaveLength(2);
|
|
362
|
+
} finally {
|
|
363
|
+
first.close();
|
|
364
|
+
second.close();
|
|
365
|
+
testServer.server.stop(true);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("drops events after a close and kills the peer", async () => {
|
|
370
|
+
const testServer = await startServer({ agentArguments: ["--ask-delay-ms=300"] });
|
|
371
|
+
const client = await connect(testServer.url);
|
|
372
|
+
try {
|
|
373
|
+
client.ask("conversation-closed", "What does the issues label mean?");
|
|
374
|
+
await client.waitFor(
|
|
375
|
+
(message) =>
|
|
376
|
+
message.type === "askChunk" && message.conversationId === "conversation-closed",
|
|
377
|
+
);
|
|
378
|
+
const mark = client.events.length;
|
|
379
|
+
client.closeAsk("conversation-closed");
|
|
380
|
+
await eventually(() => askLog(testServer.logPath).exits === 1);
|
|
381
|
+
// One socket delivers in order: the reply to this command proves every
|
|
382
|
+
// frame produced before the peer died has already arrived.
|
|
383
|
+
client.send({ type: "getProject" });
|
|
384
|
+
await client.waitFor((message) => message.type === "project");
|
|
385
|
+
const after = client.events.slice(mark);
|
|
386
|
+
expect(
|
|
387
|
+
after.every((event) => event.conversationId !== "conversation-closed"),
|
|
388
|
+
).toBe(true);
|
|
389
|
+
|
|
390
|
+
// A fresh conversation starts clean: no replayed turns from the closed one.
|
|
391
|
+
client.ask("conversation-fresh", "Which file renders it?");
|
|
392
|
+
await client.waitFor(
|
|
393
|
+
(message) =>
|
|
394
|
+
message.type === "askDone" && message.conversationId === "conversation-fresh",
|
|
395
|
+
);
|
|
396
|
+
const prompts = askLog(testServer.logPath).prompts;
|
|
397
|
+
expect(prompts).toHaveLength(2);
|
|
398
|
+
expect(askLog(testServer.logPath).texts[1]).not.toContain("Earlier in this conversation:");
|
|
399
|
+
expect(askLog(testServer.logPath).spawns).toBe(2);
|
|
400
|
+
} finally {
|
|
401
|
+
client.close();
|
|
402
|
+
testServer.server.stop(true);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("closes every peer of a socket that disconnects", async () => {
|
|
407
|
+
const testServer = await startServer({ agentArguments: ["--ask-delay-ms=300"] });
|
|
408
|
+
const client = await connect(testServer.url);
|
|
409
|
+
client.ask("conversation-socket", "What does the issues label mean?");
|
|
410
|
+
await client.waitFor(
|
|
411
|
+
(message) =>
|
|
412
|
+
message.type === "askChunk" && message.conversationId === "conversation-socket",
|
|
413
|
+
);
|
|
414
|
+
client.close();
|
|
415
|
+
await eventually(() => askLog(testServer.logPath).exits === 1);
|
|
416
|
+
testServer.server.stop(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("reports a start failure and accepts a later ask", async () => {
|
|
420
|
+
const testServer = await startServer({ command: "devboard-ask-missing-agent" });
|
|
421
|
+
const client = await connect(testServer.url);
|
|
422
|
+
try {
|
|
423
|
+
client.ask("conversation-missing", "What does the issues label mean?");
|
|
424
|
+
const failed = await client.waitFor(
|
|
425
|
+
(message) =>
|
|
426
|
+
message.type === "askError" && message.conversationId === "conversation-missing",
|
|
427
|
+
);
|
|
428
|
+
expect(String(failed.message).length).toBeGreaterThan(0);
|
|
429
|
+
// The conversation is idle again, so the next ask is not a busy refusal.
|
|
430
|
+
client.ask("conversation-missing", "Try again?");
|
|
431
|
+
const retried = await client.waitFor(
|
|
432
|
+
(message) =>
|
|
433
|
+
message.type === "askError" && message.conversationId === "conversation-missing",
|
|
434
|
+
);
|
|
435
|
+
expect(String(retried.message)).not.toContain("still answering");
|
|
436
|
+
expect(existsSync(testServer.recordPath)).toBe(false);
|
|
437
|
+
} finally {
|
|
438
|
+
client.close();
|
|
439
|
+
testServer.server.stop(true);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("recovers from an exited peer with the visible conversation", async () => {
|
|
444
|
+
const testServer = await startServer({ agentArguments: ["--ask-exit"] });
|
|
445
|
+
const client = await connect(testServer.url);
|
|
446
|
+
try {
|
|
447
|
+
client.ask("conversation-exit", "What does the issues label mean?");
|
|
448
|
+
await client.waitFor(
|
|
449
|
+
(message) =>
|
|
450
|
+
message.type === "askError" && message.conversationId === "conversation-exit",
|
|
451
|
+
);
|
|
452
|
+
client.ask("conversation-exit", "Which file renders it?");
|
|
453
|
+
await client.waitFor(
|
|
454
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-exit",
|
|
455
|
+
);
|
|
456
|
+
const logged = askLog(testServer.logPath);
|
|
457
|
+
expect(logged.spawns).toBe(2);
|
|
458
|
+
expect(logged.texts[1]).toContain("Earlier in this conversation:");
|
|
459
|
+
expect(logged.texts[1]).toContain("Developer: What does the issues label mean?");
|
|
460
|
+
const answer = chunks(client, "conversation-exit");
|
|
461
|
+
expect(answer.includes("The issues indicator shows")).toBe(true);
|
|
462
|
+
expect(answer.includes("Considering the captured element")).toBe(false);
|
|
463
|
+
} finally {
|
|
464
|
+
client.close();
|
|
465
|
+
testServer.server.stop(true);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("recovers from an alive peer that refuses the handshake", async () => {
|
|
470
|
+
for (const mode of ["--ask-refuse-initialize", "--ask-no-session"]) {
|
|
471
|
+
const testServer = await startServer({ agentArguments: [mode] });
|
|
472
|
+
const client = await connect(testServer.url);
|
|
473
|
+
try {
|
|
474
|
+
client.ask("conversation-handshake", "What does the issues label mean?");
|
|
475
|
+
const failed = await client.waitFor(
|
|
476
|
+
(message) =>
|
|
477
|
+
message.type === "askError" &&
|
|
478
|
+
message.conversationId === "conversation-handshake",
|
|
479
|
+
);
|
|
480
|
+
expect(String(failed.message).length).toBeGreaterThan(0);
|
|
481
|
+
|
|
482
|
+
// The process is still alive, but the peer has no session; the next ask
|
|
483
|
+
// must start a new peer and replay the conversation instead of taking
|
|
484
|
+
// the follow-up path of the sessionless one.
|
|
485
|
+
client.ask("conversation-handshake", "Which file renders it?");
|
|
486
|
+
await client.waitFor(
|
|
487
|
+
(message) =>
|
|
488
|
+
message.type === "askDone" && message.conversationId === "conversation-handshake",
|
|
489
|
+
);
|
|
490
|
+
const logged = askLog(testServer.logPath);
|
|
491
|
+
expect(logged.spawns).toBe(2);
|
|
492
|
+
expect(logged.prompts).toHaveLength(1);
|
|
493
|
+
expect(logged.texts[0]).toContain("Earlier in this conversation:");
|
|
494
|
+
expect(logged.texts[0]).toContain("Developer: What does the issues label mean?");
|
|
495
|
+
expect(logged.texts[0]).toContain("Question: Which file renders it?");
|
|
496
|
+
const answer = chunks(client, "conversation-handshake");
|
|
497
|
+
expect(answer.includes("The issues indicator shows")).toBe(true);
|
|
498
|
+
} finally {
|
|
499
|
+
client.close();
|
|
500
|
+
testServer.server.stop(true);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("recovers from a rejected prompt and keeps the conversation", async () => {
|
|
506
|
+
const testServer = await startServer({ agentArguments: ["--ask-fail"] });
|
|
507
|
+
const client = await connect(testServer.url);
|
|
508
|
+
try {
|
|
509
|
+
client.ask("conversation-fail", "What does the issues label mean?");
|
|
510
|
+
const failed = await client.waitFor(
|
|
511
|
+
(message) =>
|
|
512
|
+
message.type === "askError" && message.conversationId === "conversation-fail",
|
|
513
|
+
);
|
|
514
|
+
expect(String(failed.message)).toContain("The ask agent failed.");
|
|
515
|
+
client.ask("conversation-fail", "Which file renders it?");
|
|
516
|
+
await client.waitFor(
|
|
517
|
+
(message) => message.type === "askDone" && message.conversationId === "conversation-fail",
|
|
518
|
+
);
|
|
519
|
+
const logged = askLog(testServer.logPath);
|
|
520
|
+
// The peer survived the rejection, so the second turn reused it.
|
|
521
|
+
expect(logged.spawns).toBe(1);
|
|
522
|
+
expect(logged.prompts).toHaveLength(2);
|
|
523
|
+
expect(logged.texts[1]).not.toContain("Captured element context:");
|
|
524
|
+
expect(existsSync(testServer.recordPath)).toBe(false);
|
|
525
|
+
} finally {
|
|
526
|
+
client.close();
|
|
527
|
+
testServer.server.stop(true);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("server stop closes every live peer", async () => {
|
|
532
|
+
const testServer = await startServer({ agentArguments: ["--ask-delay-ms=400"] });
|
|
533
|
+
const client = await connect(testServer.url);
|
|
534
|
+
client.ask("conversation-stop", "What does the issues label mean?");
|
|
535
|
+
await client.waitFor(
|
|
536
|
+
(message) =>
|
|
537
|
+
message.type === "askChunk" && message.conversationId === "conversation-stop",
|
|
538
|
+
);
|
|
539
|
+
testServer.server.stop(true);
|
|
540
|
+
await eventually(() => askLog(testServer.logPath).exits === 1);
|
|
541
|
+
expect(existsSync(testServer.recordPath)).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
});
|