switchroom 0.7.15 → 0.10.0
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/README.md +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — lifecycle integration. Drive the real driver with
|
|
3
|
-
* TWO_ZONE_CARD=1 set; assert the rendered HTML for a turn with 2
|
|
4
|
-
* fleet members contains expected substrings (header phase, parent
|
|
5
|
-
* bullets, fleet rows, fleet count).
|
|
6
|
-
*
|
|
7
|
-
* Uses the same lightweight harness pattern as
|
|
8
|
-
* progress-card-driver-fleet-shadow.test.ts — no Telegram bot, just
|
|
9
|
-
* record the emit calls and inspect their HTML payload.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
13
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
14
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
15
|
-
|
|
16
|
-
function harness() {
|
|
17
|
-
let now = 1000
|
|
18
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
19
|
-
let nextRef = 0
|
|
20
|
-
const emits: Array<{ chatId: string; payload: string }> = []
|
|
21
|
-
const driver = createProgressDriver({
|
|
22
|
-
emit: (args) => {
|
|
23
|
-
// Parent card emits only — sub-agent per-agent cards carry agentId.
|
|
24
|
-
if ((args as { agentId?: string }).agentId == null) {
|
|
25
|
-
emits.push({ chatId: args.chatId, payload: args.html })
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
minIntervalMs: 0,
|
|
29
|
-
coalesceMs: 0,
|
|
30
|
-
initialDelayMs: 0,
|
|
31
|
-
promoteAfterMs: 999_999,
|
|
32
|
-
now: () => now,
|
|
33
|
-
setTimeout: (fn, ms) => {
|
|
34
|
-
const ref = nextRef++
|
|
35
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
36
|
-
return { ref }
|
|
37
|
-
},
|
|
38
|
-
clearTimeout: (h) => {
|
|
39
|
-
const ref = (h as { ref: number }).ref
|
|
40
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
41
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
42
|
-
},
|
|
43
|
-
setInterval: (fn, ms) => {
|
|
44
|
-
const ref = nextRef++
|
|
45
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
46
|
-
return { ref }
|
|
47
|
-
},
|
|
48
|
-
clearInterval: (h) => {
|
|
49
|
-
const ref = (h as { ref: number }).ref
|
|
50
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
51
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
return {
|
|
55
|
-
driver,
|
|
56
|
-
emits,
|
|
57
|
-
advance: (ms: number) => {
|
|
58
|
-
now += ms
|
|
59
|
-
const due = timers.filter((t) => t.fireAt <= now)
|
|
60
|
-
for (const t of due) {
|
|
61
|
-
t.fn()
|
|
62
|
-
if (t.repeat) {
|
|
63
|
-
t.fireAt = now + t.repeat
|
|
64
|
-
} else {
|
|
65
|
-
const i = timers.indexOf(t)
|
|
66
|
-
if (i >= 0) timers.splice(i, 1)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
flush: () => {
|
|
71
|
-
// Pump any pending timers
|
|
72
|
-
const due = timers.filter((t) => t.fireAt <= now)
|
|
73
|
-
for (const t of due) t.fn()
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
79
|
-
kind: 'enqueue',
|
|
80
|
-
chatId,
|
|
81
|
-
messageId: '1',
|
|
82
|
-
threadId: null,
|
|
83
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('two-zone-card lifecycle (TWO_ZONE_CARD=1)', () => {
|
|
87
|
-
let prevFlag: string | undefined
|
|
88
|
-
beforeEach(() => {
|
|
89
|
-
prevFlag = process.env.TWO_ZONE_CARD
|
|
90
|
-
process.env.TWO_ZONE_CARD = '1'
|
|
91
|
-
})
|
|
92
|
-
afterEach(() => {
|
|
93
|
-
if (prevFlag === undefined) delete process.env.TWO_ZONE_CARD
|
|
94
|
-
else process.env.TWO_ZONE_CARD = prevFlag
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('renders two-zone card with fleet rows when flag is on', () => {
|
|
98
|
-
const { driver, emits, advance } = harness()
|
|
99
|
-
const CHAT = 'c1'
|
|
100
|
-
driver.ingest(enqueue(CHAT), null)
|
|
101
|
-
|
|
102
|
-
const events: SessionEvent[] = [
|
|
103
|
-
{ kind: 'tool_use', toolUseId: 'p1', toolName: 'Read', input: { file_path: '/tmp/foo.ts' } },
|
|
104
|
-
{ kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'do work', subagentType: 'worker' },
|
|
105
|
-
{ kind: 'sub_agent_started', agentId: 'sa2', firstPromptText: 'review', subagentType: 'reviewer' },
|
|
106
|
-
{ kind: 'sub_agent_tool_use', agentId: 'sa1', toolUseId: 't1', toolName: 'Grep', input: { pattern: 'TODO' } },
|
|
107
|
-
]
|
|
108
|
-
for (const ev of events) driver.ingest(ev, CHAT)
|
|
109
|
-
// Drain the coalesce/min-interval setTimeout queue so deferred
|
|
110
|
-
// sub-agent emits flush. Each ingest schedules a 0-delay timer
|
|
111
|
-
// that is only invoked when fake time advances.
|
|
112
|
-
advance(0)
|
|
113
|
-
|
|
114
|
-
// Find the most recent emitted payload — it should be a two-zone card.
|
|
115
|
-
const last = emits[emits.length - 1]
|
|
116
|
-
expect(last).toBeDefined()
|
|
117
|
-
const html = last.payload
|
|
118
|
-
// Header substrings
|
|
119
|
-
expect(html).toMatch(/Working/)
|
|
120
|
-
// Fleet zone present with count
|
|
121
|
-
expect(html).toContain('FLEET (2)')
|
|
122
|
-
expect(html).toContain('worker')
|
|
123
|
-
expect(html).toContain('reviewer')
|
|
124
|
-
// Fleet ids (6 chars)
|
|
125
|
-
expect(html).toContain('sa1')
|
|
126
|
-
expect(html).toContain('sa2')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — renderer output never reintroduces raw absolute paths
|
|
3
|
-
* or bearer-shaped tokens. Most coverage lives in fleet-state.test.ts;
|
|
4
|
-
* this asserts the *renderer* basenames/redacts via the FleetMember's
|
|
5
|
-
* sanitised values (i.e. it doesn't re-pull from raw input anywhere).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { renderTwoZoneCard } from '../two-zone-card.js'
|
|
10
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
11
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
12
|
-
|
|
13
|
-
const baseState: ProgressCardState = {
|
|
14
|
-
turnStartedAt: 1,
|
|
15
|
-
items: [],
|
|
16
|
-
narratives: [],
|
|
17
|
-
stage: 'run',
|
|
18
|
-
thinking: false,
|
|
19
|
-
subAgents: new Map(),
|
|
20
|
-
pendingAgentSpawns: new Map(),
|
|
21
|
-
tasks: [],
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function fm(over: Partial<FleetMember>): FleetMember {
|
|
25
|
-
return {
|
|
26
|
-
agentId: 'aaaaaaaaaaaa',
|
|
27
|
-
role: 'agent',
|
|
28
|
-
startedAt: 0,
|
|
29
|
-
toolCount: 1,
|
|
30
|
-
lastActivityAt: 1000,
|
|
31
|
-
lastTool: null,
|
|
32
|
-
status: 'running',
|
|
33
|
-
terminalAt: null,
|
|
34
|
-
errorSeen: false,
|
|
35
|
-
originatingTurnKey: 'k',
|
|
36
|
-
...over,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('two-zone-card sanitise', () => {
|
|
41
|
-
it('does not contain raw absolute path under /etc/secrets', () => {
|
|
42
|
-
const fleet = new Map([['a', fm({
|
|
43
|
-
lastTool: { name: 'Read', sanitisedArg: 'foo.key' }, // already sanitised by fleet-state
|
|
44
|
-
})]])
|
|
45
|
-
const out = renderTwoZoneCard({ state: baseState, fleet, now: 2000 })
|
|
46
|
-
expect(out).not.toContain('/etc/secrets')
|
|
47
|
-
expect(out).toContain('foo.key')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('does not contain bearer-shaped tokens (sanitised upstream)', () => {
|
|
51
|
-
const fleet = new Map([['a', fm({
|
|
52
|
-
lastTool: { name: 'Bash', sanitisedArg: 'curl -H "Authorization: [redacted]" https://api' },
|
|
53
|
-
})]])
|
|
54
|
-
const out = renderTwoZoneCard({ state: baseState, fleet, now: 2000 })
|
|
55
|
-
expect(out).toContain('[redacted]')
|
|
56
|
-
expect(out).not.toMatch(/Bearer\s+[A-Za-z0-9]{16,}/)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — golden output for 5 canonical card states.
|
|
3
|
-
*
|
|
4
|
-
* Uses explicit `toBe()` rather than `toMatchSnapshot()` so the same
|
|
5
|
-
* test file passes under both vitest (Core tests CI step) and bun
|
|
6
|
-
* (Plugin tests CI step) — the two snapshot formats are incompatible.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest'
|
|
10
|
-
import { renderTwoZoneCard } from '../two-zone-card.js'
|
|
11
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
12
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
13
|
-
|
|
14
|
-
function st(over: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
|
|
15
|
-
return {
|
|
16
|
-
turnStartedAt: 0,
|
|
17
|
-
items: [],
|
|
18
|
-
narratives: [],
|
|
19
|
-
stage: over.stage,
|
|
20
|
-
thinking: false,
|
|
21
|
-
subAgents: new Map(),
|
|
22
|
-
pendingAgentSpawns: new Map(),
|
|
23
|
-
tasks: [],
|
|
24
|
-
...over,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function fm(over: Partial<FleetMember>): FleetMember {
|
|
29
|
-
return {
|
|
30
|
-
agentId: 'aaaaaa00',
|
|
31
|
-
role: 'agent',
|
|
32
|
-
startedAt: 0,
|
|
33
|
-
toolCount: 0,
|
|
34
|
-
lastActivityAt: 0,
|
|
35
|
-
lastTool: null,
|
|
36
|
-
status: 'running',
|
|
37
|
-
terminalAt: null,
|
|
38
|
-
errorSeen: false,
|
|
39
|
-
originatingTurnKey: 'k',
|
|
40
|
-
...over,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const NOW = 60_000
|
|
45
|
-
|
|
46
|
-
describe('two-zone-card snapshots', () => {
|
|
47
|
-
it('empty fleet — clean clerk-style card', () => {
|
|
48
|
-
const out = renderTwoZoneCard({
|
|
49
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 5000 }),
|
|
50
|
-
fleet: new Map(),
|
|
51
|
-
now: NOW,
|
|
52
|
-
})
|
|
53
|
-
expect(out).toBe('⚙️ <b>Working…</b> · ⏱ 00:05 · 🔧 0')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('3 members mixed', () => {
|
|
57
|
-
const fleet = new Map([
|
|
58
|
-
['a', fm({ agentId: 'aaaaaa01', role: 'researcher', status: 'running', toolCount: 4, lastActivityAt: NOW - 2000, lastTool: { name: 'Grep', sanitisedArg: 'TODO' } })],
|
|
59
|
-
['b', fm({ agentId: 'bbbbbb02', role: 'worker', status: 'done', toolCount: 8, lastActivityAt: NOW - 10_000, terminalAt: NOW - 10_000 })],
|
|
60
|
-
['c', fm({ agentId: 'cccccc03', role: 'reviewer', status: 'stuck', toolCount: 2, lastActivityAt: NOW - 70_000, lastTool: { name: 'Read', sanitisedArg: 'big.ts' } })],
|
|
61
|
-
])
|
|
62
|
-
const out = renderTwoZoneCard({
|
|
63
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 30_000 }),
|
|
64
|
-
fleet,
|
|
65
|
-
now: NOW,
|
|
66
|
-
})
|
|
67
|
-
expect(out).toBe(
|
|
68
|
-
'⚙️ <b>Working…</b> · ⏱ 00:30 · 🔧 14 · 🤖 3\n' +
|
|
69
|
-
'\n' +
|
|
70
|
-
'<b>FLEET (3)</b>\n' +
|
|
71
|
-
'↻ researcher <code>aaaaaa</code> · 4t · Grep <code>TODO</code> (2s ago)\n' +
|
|
72
|
-
'✓ worker <code>bbbbbb</code> · 8t · done 10s ago\n' +
|
|
73
|
-
'⚠ reviewer <code>cccccc</code> · 2t · idle 1m10s ago',
|
|
74
|
-
)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('all-done with completed receipts', () => {
|
|
78
|
-
const fleet = new Map([
|
|
79
|
-
['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'done', toolCount: 5, lastActivityAt: NOW - 10_000, terminalAt: NOW - 10_000 })],
|
|
80
|
-
['b', fm({ agentId: 'bbbbbb02', role: 'reviewer', status: 'done', toolCount: 3, lastActivityAt: NOW - 5000, terminalAt: NOW - 5000 })],
|
|
81
|
-
])
|
|
82
|
-
const out = renderTwoZoneCard({
|
|
83
|
-
state: st({ stage: 'done', turnStartedAt: NOW - 20_000 }),
|
|
84
|
-
fleet,
|
|
85
|
-
now: NOW,
|
|
86
|
-
})
|
|
87
|
-
expect(out).toBe(
|
|
88
|
-
'✅ <b>Done</b> · ⏱ 00:20 · 🔧 8 · 🤖 2\n' +
|
|
89
|
-
'\n' +
|
|
90
|
-
'<b>FLEET (2)</b>\n' +
|
|
91
|
-
'✓ reviewer <code>bbbbbb</code> · 3t · done 5s ago\n' +
|
|
92
|
-
'✓ worker <code>aaaaaa</code> · 5t · done 10s ago',
|
|
93
|
-
)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('all-stuck', () => {
|
|
97
|
-
const fleet = new Map([
|
|
98
|
-
['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'stuck', toolCount: 1, lastActivityAt: NOW - 90_000, lastTool: { name: 'Bash', sanitisedArg: 'sleep 999' } })],
|
|
99
|
-
['b', fm({ agentId: 'bbbbbb02', role: 'worker', status: 'stuck', toolCount: 1, lastActivityAt: NOW - 80_000, lastTool: { name: 'Bash', sanitisedArg: 'sleep 999' } })],
|
|
100
|
-
])
|
|
101
|
-
const out = renderTwoZoneCard({
|
|
102
|
-
state: st({ stage: 'run', turnStartedAt: NOW - 95_000 }),
|
|
103
|
-
fleet,
|
|
104
|
-
now: NOW,
|
|
105
|
-
})
|
|
106
|
-
expect(out).toBe(
|
|
107
|
-
'⚠ <b>Stalled</b> · ⏱ 01:35 · 🔧 2 · 🤖 2\n' +
|
|
108
|
-
'\n' +
|
|
109
|
-
'<b>FLEET (2)</b>\n' +
|
|
110
|
-
'⚠ worker <code>bbbbbb</code> · 1t · idle 1m20s ago\n' +
|
|
111
|
-
'⚠ worker <code>aaaaaa</code> · 1t · idle 1m30s ago',
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('background — parent done, background sub still running', () => {
|
|
116
|
-
const fleet = new Map([
|
|
117
|
-
['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'done', toolCount: 5, lastActivityAt: NOW - 30_000, terminalAt: NOW - 30_000 })],
|
|
118
|
-
['b', fm({ agentId: 'bbbbbb02', role: 'background', status: 'background', toolCount: 12, lastActivityAt: NOW - 1000, lastTool: { name: 'Bash', sanitisedArg: 'long-job.sh' } })],
|
|
119
|
-
])
|
|
120
|
-
const out = renderTwoZoneCard({
|
|
121
|
-
state: st({ stage: 'done', turnStartedAt: NOW - 90_000 }),
|
|
122
|
-
fleet,
|
|
123
|
-
now: NOW,
|
|
124
|
-
})
|
|
125
|
-
expect(out).toBe(
|
|
126
|
-
'⏸ <b>Background</b> · ⏱ 01:30 · 🔧 17 · 🤖 2\n' +
|
|
127
|
-
'\n' +
|
|
128
|
-
'<b>FLEET (2)</b>\n' +
|
|
129
|
-
'⏸ background <code>bbbbbb</code> · 12t · Bash <code>long-job.sh</code> (1s ago)\n' +
|
|
130
|
-
'✓ worker <code>aaaaaa</code> · 5t · done 30s ago',
|
|
131
|
-
)
|
|
132
|
-
})
|
|
133
|
-
})
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2 of #662 — concurrent-chat isolation. Two distinct chats each
|
|
3
|
-
* spawn a background sub-agent. Routing must keep their fleets
|
|
4
|
-
* completely independent: a tool_use event for chat A's bg sub-agent
|
|
5
|
-
* must never bleed into chat B's fleet, and vice versa.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
10
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
11
|
-
|
|
12
|
-
function harness() {
|
|
13
|
-
let now = 1000
|
|
14
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
15
|
-
let nextRef = 0
|
|
16
|
-
const driver = createProgressDriver({
|
|
17
|
-
emit: () => {},
|
|
18
|
-
minIntervalMs: 500,
|
|
19
|
-
coalesceMs: 400,
|
|
20
|
-
initialDelayMs: 0,
|
|
21
|
-
promoteAfterMs: 999_999,
|
|
22
|
-
now: () => now,
|
|
23
|
-
setTimeout: (fn, ms) => {
|
|
24
|
-
const ref = nextRef++
|
|
25
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
26
|
-
return { ref }
|
|
27
|
-
},
|
|
28
|
-
clearTimeout: (h) => {
|
|
29
|
-
const ref = (h as { ref: number }).ref
|
|
30
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
31
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
32
|
-
},
|
|
33
|
-
setInterval: (fn, ms) => {
|
|
34
|
-
const ref = nextRef++
|
|
35
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
36
|
-
return { ref }
|
|
37
|
-
},
|
|
38
|
-
clearInterval: (h) => {
|
|
39
|
-
const ref = (h as { ref: number }).ref
|
|
40
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
41
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
return { driver, advance: (ms: number) => { now += ms } }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const enqueue = (chatId: string): SessionEvent => ({
|
|
48
|
-
kind: 'enqueue',
|
|
49
|
-
chatId,
|
|
50
|
-
messageId: '1',
|
|
51
|
-
threadId: null,
|
|
52
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const enqueueWithThread = (chatId: string, threadId: string, msgId: string): SessionEvent => ({
|
|
56
|
-
kind: 'enqueue',
|
|
57
|
-
chatId,
|
|
58
|
-
messageId: msgId,
|
|
59
|
-
threadId,
|
|
60
|
-
rawContent: `<channel chat_id="${chatId}" thread_id="${threadId}">go</channel>`,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe('P2: concurrent-chat fleet isolation', () => {
|
|
64
|
-
it('two chats with their own background sub-agents do not cross-pollinate', () => {
|
|
65
|
-
const { driver } = harness()
|
|
66
|
-
|
|
67
|
-
// Chat A
|
|
68
|
-
driver.ingest(enqueue('cA'), null)
|
|
69
|
-
driver.ingest(
|
|
70
|
-
{
|
|
71
|
-
kind: 'tool_use',
|
|
72
|
-
toolName: 'Agent',
|
|
73
|
-
toolUseId: 'tuA',
|
|
74
|
-
input: { prompt: 'pA', run_in_background: true },
|
|
75
|
-
},
|
|
76
|
-
'cA',
|
|
77
|
-
)
|
|
78
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saA', firstPromptText: 'pA' }, 'cA')
|
|
79
|
-
|
|
80
|
-
// Chat B
|
|
81
|
-
driver.ingest(enqueue('cB'), null)
|
|
82
|
-
driver.ingest(
|
|
83
|
-
{
|
|
84
|
-
kind: 'tool_use',
|
|
85
|
-
toolName: 'Agent',
|
|
86
|
-
toolUseId: 'tuB',
|
|
87
|
-
input: { prompt: 'pB', run_in_background: true },
|
|
88
|
-
},
|
|
89
|
-
'cB',
|
|
90
|
-
)
|
|
91
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saB', firstPromptText: 'pB' }, 'cB')
|
|
92
|
-
|
|
93
|
-
const fleetA = driver.peekFleet('cA')!
|
|
94
|
-
const fleetB = driver.peekFleet('cB')!
|
|
95
|
-
expect(fleetA.has('saA')).toBe(true)
|
|
96
|
-
expect(fleetA.has('saB')).toBe(false)
|
|
97
|
-
expect(fleetB.has('saB')).toBe(true)
|
|
98
|
-
expect(fleetB.has('saA')).toBe(false)
|
|
99
|
-
expect(fleetA.get('saA')!.status).toBe('background')
|
|
100
|
-
expect(fleetB.get('saB')!.status).toBe('background')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('PR-C2: two threads in the SAME chat (different threadId, shared chatId) — no cross-talk in per-chat state maps', () => {
|
|
104
|
-
// Two forum-topic threads in the same Telegram chat. Same chatId,
|
|
105
|
-
// distinct threadId. The driver must key per-turn state on the
|
|
106
|
-
// composite (chatId, threadId) base — closing one thread's turn
|
|
107
|
-
// must not touch the other's, and bg sub-agents must not bleed
|
|
108
|
-
// between threads.
|
|
109
|
-
const { driver } = harness()
|
|
110
|
-
|
|
111
|
-
driver.ingest(enqueueWithThread('cShared', 'tA', '1'), null)
|
|
112
|
-
driver.ingest(
|
|
113
|
-
{
|
|
114
|
-
kind: 'tool_use', toolName: 'Agent', toolUseId: 'tuA',
|
|
115
|
-
input: { prompt: 'pA', run_in_background: true },
|
|
116
|
-
},
|
|
117
|
-
'cShared',
|
|
118
|
-
)
|
|
119
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saA', firstPromptText: 'pA' }, 'cShared')
|
|
120
|
-
|
|
121
|
-
driver.ingest(enqueueWithThread('cShared', 'tB', '2'), null)
|
|
122
|
-
driver.ingest(
|
|
123
|
-
{
|
|
124
|
-
kind: 'tool_use', toolName: 'Agent', toolUseId: 'tuB',
|
|
125
|
-
input: { prompt: 'pB', run_in_background: true },
|
|
126
|
-
},
|
|
127
|
-
'cShared',
|
|
128
|
-
)
|
|
129
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saB', firstPromptText: 'pB' }, 'cShared')
|
|
130
|
-
|
|
131
|
-
// peekFleet keys on (chatId, threadId).
|
|
132
|
-
const fleetA = driver.peekFleet('cShared', 'tA')!
|
|
133
|
-
const fleetB = driver.peekFleet('cShared', 'tB')!
|
|
134
|
-
expect(fleetA).toBeDefined()
|
|
135
|
-
expect(fleetB).toBeDefined()
|
|
136
|
-
expect(fleetA.has('saA')).toBe(true)
|
|
137
|
-
expect(fleetA.has('saB')).toBe(false)
|
|
138
|
-
expect(fleetB.has('saB')).toBe(true)
|
|
139
|
-
expect(fleetB.has('saA')).toBe(false)
|
|
140
|
-
|
|
141
|
-
// baseTurnSeqs must have distinct entries for the two threads.
|
|
142
|
-
const maps = (driver as unknown as {
|
|
143
|
-
_debugGetMaps?: () => { baseTurnSeqs: Map<string, number>; chats: Map<string, unknown> }
|
|
144
|
-
})._debugGetMaps!()
|
|
145
|
-
const baseKeys = [...maps.baseTurnSeqs.keys()]
|
|
146
|
-
// The two threads MUST resolve to distinct base keys (proves the
|
|
147
|
-
// composite (chatId, threadId) is honoured). The exact base-key
|
|
148
|
-
// format is (chatId:threadId) — assert distinctness without
|
|
149
|
-
// hard-coding the format.
|
|
150
|
-
expect(baseKeys.length).toBe(2)
|
|
151
|
-
expect(new Set(baseKeys).size).toBe(2)
|
|
152
|
-
// chats map: 2 in-flight turns.
|
|
153
|
-
expect(maps.chats.size).toBe(2)
|
|
154
|
-
})
|
|
155
|
-
})
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PR-A — phaseFor precedence: silent-end must be lifted above the
|
|
3
|
-
* background/done branches but gated on parentDone (or stage===done) so
|
|
4
|
-
* it can't fire while the parent is still in flight.
|
|
5
|
-
*
|
|
6
|
-
* Drives `phaseFor` across all combinations of
|
|
7
|
-
* (parentDone, silentEnd, fleetRunning, stalledClose) and asserts the
|
|
8
|
-
* resolved label.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from 'vitest'
|
|
12
|
-
import { phaseFor } from '../two-zone-card.js'
|
|
13
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
14
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
15
|
-
|
|
16
|
-
function fm(id: string, status: FleetMember['status'], lastActivityAt: number = 100_000): FleetMember {
|
|
17
|
-
return {
|
|
18
|
-
agentId: id,
|
|
19
|
-
role: 'agent',
|
|
20
|
-
startedAt: 0,
|
|
21
|
-
toolCount: 0,
|
|
22
|
-
lastActivityAt,
|
|
23
|
-
lastTool: null,
|
|
24
|
-
status,
|
|
25
|
-
terminalAt: status === 'done' || status === 'failed' || status === 'killed' ? lastActivityAt : null,
|
|
26
|
-
errorSeen: status === 'failed',
|
|
27
|
-
originatingTurnKey: 'k',
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function st(stage: ProgressCardState['stage']): ProgressCardState {
|
|
32
|
-
return {
|
|
33
|
-
turnStartedAt: 1,
|
|
34
|
-
items: [],
|
|
35
|
-
narratives: [],
|
|
36
|
-
stage,
|
|
37
|
-
thinking: false,
|
|
38
|
-
subAgents: new Map(),
|
|
39
|
-
pendingAgentSpawns: new Map(),
|
|
40
|
-
tasks: [],
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const NOW = 100_000
|
|
45
|
-
|
|
46
|
-
interface Row {
|
|
47
|
-
parentDone: boolean
|
|
48
|
-
silentEnd: boolean
|
|
49
|
-
fleetRunning: boolean
|
|
50
|
-
stalledClose: boolean
|
|
51
|
-
expected: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Truth table: 2^4 = 16 combinations of the four boolean inputs.
|
|
55
|
-
// stalledClose dominates everything → Forced close.
|
|
56
|
-
// silentEnd fires only when parentDone is true.
|
|
57
|
-
// When silentEnd is gated off and parent is still running, fleetRunning
|
|
58
|
-
// alone yields Working… (parent active); when parent is done +
|
|
59
|
-
// fleetRunning we get Background; parentDone alone yields Done.
|
|
60
|
-
const rows: Row[] = [
|
|
61
|
-
// stalledClose=true → always Forced close
|
|
62
|
-
{ parentDone: false, silentEnd: false, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
|
|
63
|
-
{ parentDone: false, silentEnd: false, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
|
|
64
|
-
{ parentDone: false, silentEnd: true, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
|
|
65
|
-
{ parentDone: false, silentEnd: true, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
|
|
66
|
-
{ parentDone: true, silentEnd: false, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
|
|
67
|
-
{ parentDone: true, silentEnd: false, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
|
|
68
|
-
{ parentDone: true, silentEnd: true, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
|
|
69
|
-
{ parentDone: true, silentEnd: true, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
|
|
70
|
-
|
|
71
|
-
// stalledClose=false
|
|
72
|
-
// parent in flight (parentDone=false)
|
|
73
|
-
{ parentDone: false, silentEnd: false, fleetRunning: false, stalledClose: false, expected: 'Working…' },
|
|
74
|
-
{ parentDone: false, silentEnd: false, fleetRunning: true, stalledClose: false, expected: 'Working…' },
|
|
75
|
-
// silentEnd is GATED on parentDone — must not fire while parent in flight
|
|
76
|
-
{ parentDone: false, silentEnd: true, fleetRunning: false, stalledClose: false, expected: 'Working…' },
|
|
77
|
-
{ parentDone: false, silentEnd: true, fleetRunning: true, stalledClose: false, expected: 'Working…' },
|
|
78
|
-
|
|
79
|
-
// parent done
|
|
80
|
-
{ parentDone: true, silentEnd: false, fleetRunning: false, stalledClose: false, expected: 'Done' },
|
|
81
|
-
{ parentDone: true, silentEnd: false, fleetRunning: true, stalledClose: false, expected: 'Background' },
|
|
82
|
-
// silentEnd LIFTED above background/done — fires even when fleet still running
|
|
83
|
-
{ parentDone: true, silentEnd: true, fleetRunning: false, stalledClose: false, expected: 'Ended without reply' },
|
|
84
|
-
{ parentDone: true, silentEnd: true, fleetRunning: true, stalledClose: false, expected: 'Ended without reply' },
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
describe('phaseFor precedence — (parentDone, silentEnd, fleetRunning, stalledClose)', () => {
|
|
88
|
-
it.each(rows)(
|
|
89
|
-
'parentDone=$parentDone silentEnd=$silentEnd fleetRunning=$fleetRunning stalledClose=$stalledClose → $expected',
|
|
90
|
-
({ parentDone, silentEnd, fleetRunning, stalledClose, expected }) => {
|
|
91
|
-
const stage: ProgressCardState['stage'] = parentDone ? 'done' : 'run'
|
|
92
|
-
const fleet = new Map<string, FleetMember>()
|
|
93
|
-
if (fleetRunning) fleet.set('a', fm('a', 'running', NOW))
|
|
94
|
-
const opts: Record<string, unknown> = {}
|
|
95
|
-
if (silentEnd) opts.silentEnd = true
|
|
96
|
-
if (stalledClose) opts.stalledClose = true
|
|
97
|
-
// parentDone is conveyed via stage; also pass the explicit flag for parity
|
|
98
|
-
if (parentDone) opts.parentDone = true
|
|
99
|
-
|
|
100
|
-
const phase = phaseFor(st(stage), fleet, NOW, opts)
|
|
101
|
-
expect(phase.label).toBe(expected)
|
|
102
|
-
},
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
// PR-C2 reviewer follow-up: add an explicit row for FleetMember.status=
|
|
106
|
-
// 'background'. The truth-table above uses status='running' to model
|
|
107
|
-
// "fleet still running"; `anyFleetActive` also returns true for
|
|
108
|
-
// 'background' status, so a parentDone+background-only fleet should
|
|
109
|
-
// resolve to "Background" rather than "Done".
|
|
110
|
-
it('parentDone=true + fleet=[background-only] → Background (not Done)', () => {
|
|
111
|
-
const fleet = new Map<string, FleetMember>([
|
|
112
|
-
['bg', fm('bg', 'background', NOW)],
|
|
113
|
-
])
|
|
114
|
-
const phase = phaseFor(st('done'), fleet, NOW, { parentDone: true })
|
|
115
|
-
expect(phase.label).toBe('Background')
|
|
116
|
-
})
|
|
117
|
-
})
|