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,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression: parent turn_end fires before bg sub-agent emits any
|
|
3
|
-
* state.subAgents entries (i.e. sub_agent_started hasn't arrived yet).
|
|
4
|
-
*
|
|
5
|
-
* Before the fix, hasAnyRunningSubAgent returned false at turn_end time
|
|
6
|
-
* (subAgents was empty) so the card was closed immediately. The fleet
|
|
7
|
-
* shadow's hasLiveBackground gate is the fix — the fleet member is
|
|
8
|
-
* created at sub_agent_started time and tagged status:'background',
|
|
9
|
-
* which keeps pendingCompletion=true even when subAgents is empty.
|
|
10
|
-
*
|
|
11
|
-
* Scenario:
|
|
12
|
-
* 1. Parent emits Agent tool_use with run_in_background:true.
|
|
13
|
-
* 2. Parent emits turn_end immediately — sub_agent_started has NOT
|
|
14
|
-
* arrived yet, so state.subAgents is empty.
|
|
15
|
-
* 3. Card must remain alive (NOT in completions).
|
|
16
|
-
* 4. sub_agent_started arrives → fleet records the member.
|
|
17
|
-
* 5. sub_agent_turn_end arrives → deferred completion must fire.
|
|
18
|
-
*/
|
|
19
|
-
import { describe, it, expect } from 'vitest'
|
|
20
|
-
import { makeHarness, enqueue } from './_progress-card-harness.js'
|
|
21
|
-
|
|
22
|
-
describe('two-zone-bg: parent turn_end before sub_agent_started → card survives → bg done cleans up', () => {
|
|
23
|
-
it('does not close the card prematurely; fires completion on bg sub-agent terminal', () => {
|
|
24
|
-
const { driver, completions, advance } = makeHarness({
|
|
25
|
-
minIntervalMs: 0,
|
|
26
|
-
coalesceMs: 0,
|
|
27
|
-
promoteAfterMs: 999_999,
|
|
28
|
-
})
|
|
29
|
-
const CHAT = 'cBG_early'
|
|
30
|
-
|
|
31
|
-
// Step 1: enqueue a new parent turn.
|
|
32
|
-
driver.ingest(enqueue(CHAT), null)
|
|
33
|
-
|
|
34
|
-
// Step 2: parent emits Agent tool_use with run_in_background:true.
|
|
35
|
-
driver.ingest(
|
|
36
|
-
{
|
|
37
|
-
kind: 'tool_use',
|
|
38
|
-
toolName: 'Agent',
|
|
39
|
-
toolUseId: 'tu-bg-1',
|
|
40
|
-
input: { prompt: 'do bg work', run_in_background: true },
|
|
41
|
-
},
|
|
42
|
-
CHAT,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
// Step 3: sub_agent_started — fleet member created as background.
|
|
46
|
-
driver.ingest(
|
|
47
|
-
{
|
|
48
|
-
kind: 'sub_agent_started',
|
|
49
|
-
agentId: 'sa-early',
|
|
50
|
-
firstPromptText: 'do bg work',
|
|
51
|
-
},
|
|
52
|
-
CHAT,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
// Step 4: parent turn_end fires — sub-agent has no subAgents reducer
|
|
56
|
-
// entry yet (the sub_agent_started above only added to fleet, the
|
|
57
|
-
// reducer may not have a running entry depending on event ordering).
|
|
58
|
-
// Regardless, fleet has a live background member → card must defer.
|
|
59
|
-
driver.ingest({ kind: 'turn_end', durationMs: 100 }, CHAT)
|
|
60
|
-
|
|
61
|
-
// Card must NOT be complete yet.
|
|
62
|
-
expect(completions).toHaveLength(0)
|
|
63
|
-
|
|
64
|
-
// Step 5: bg sub-agent does some work.
|
|
65
|
-
advance(10)
|
|
66
|
-
driver.ingest(
|
|
67
|
-
{
|
|
68
|
-
kind: 'sub_agent_tool_use',
|
|
69
|
-
agentId: 'sa-early',
|
|
70
|
-
toolUseId: 'bgt-1',
|
|
71
|
-
toolName: 'Bash',
|
|
72
|
-
input: { command: 'echo hi' },
|
|
73
|
-
},
|
|
74
|
-
CHAT,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// Still not done.
|
|
78
|
-
expect(completions).toHaveLength(0)
|
|
79
|
-
|
|
80
|
-
// Step 6: bg sub-agent terminates → deferred completion must fire.
|
|
81
|
-
advance(10)
|
|
82
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'sa-early' }, CHAT)
|
|
83
|
-
|
|
84
|
-
// Completion must have fired exactly once.
|
|
85
|
-
expect(completions).toHaveLength(1)
|
|
86
|
-
})
|
|
87
|
-
})
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P2 of #662 / fixes #64 — background sub-agent persistence across
|
|
3
|
-
* subsequent parent turns.
|
|
4
|
-
*
|
|
5
|
-
* Lifecycle under test:
|
|
6
|
-
* - Turn A enqueue → parent dispatches Agent({run_in_background:true})
|
|
7
|
-
* → sub_agent_started → parent reply → turn_end (would normally
|
|
8
|
-
* finalize and dispose).
|
|
9
|
-
* - Turn B enqueues. The original PerChatState for turn A must
|
|
10
|
-
* survive because its fleet still has a 'background' member.
|
|
11
|
-
* - Background sub-agent emits sub_agent_tool_use. Routing must land
|
|
12
|
-
* the event on turn A's state (originatingTurnKey), NOT turn B's
|
|
13
|
-
* fresh state.
|
|
14
|
-
* - When the background sub-agent finally fires sub_agent_turn_end,
|
|
15
|
-
* turn A's PerChatState completes and is disposed.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect } from 'vitest'
|
|
19
|
-
import { createProgressDriver } from '../progress-card-driver.js'
|
|
20
|
-
import type { SessionEvent } from '../session-tail.js'
|
|
21
|
-
|
|
22
|
-
function harness() {
|
|
23
|
-
let now = 1000
|
|
24
|
-
const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
|
|
25
|
-
let nextRef = 0
|
|
26
|
-
const completions: string[] = []
|
|
27
|
-
const driver = createProgressDriver({
|
|
28
|
-
emit: () => {},
|
|
29
|
-
minIntervalMs: 500,
|
|
30
|
-
coalesceMs: 400,
|
|
31
|
-
initialDelayMs: 0,
|
|
32
|
-
promoteAfterMs: 999_999,
|
|
33
|
-
onTurnComplete: (s) => completions.push(s.turnKey),
|
|
34
|
-
now: () => now,
|
|
35
|
-
setTimeout: (fn, ms) => {
|
|
36
|
-
const ref = nextRef++
|
|
37
|
-
timers.push({ fireAt: now + ms, fn, ref })
|
|
38
|
-
return { ref }
|
|
39
|
-
},
|
|
40
|
-
clearTimeout: (h) => {
|
|
41
|
-
const ref = (h as { ref: number }).ref
|
|
42
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
43
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
44
|
-
},
|
|
45
|
-
setInterval: (fn, ms) => {
|
|
46
|
-
const ref = nextRef++
|
|
47
|
-
timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
|
|
48
|
-
return { ref }
|
|
49
|
-
},
|
|
50
|
-
clearInterval: (h) => {
|
|
51
|
-
const ref = (h as { ref: number }).ref
|
|
52
|
-
const idx = timers.findIndex((t) => t.ref === ref)
|
|
53
|
-
if (idx !== -1) timers.splice(idx, 1)
|
|
54
|
-
},
|
|
55
|
-
})
|
|
56
|
-
return { driver, completions, advance: (ms: number) => { now += ms } }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const enqueue = (chatId: string, msgId: string): SessionEvent => ({
|
|
60
|
-
kind: 'enqueue',
|
|
61
|
-
chatId,
|
|
62
|
-
messageId: msgId,
|
|
63
|
-
threadId: null,
|
|
64
|
-
rawContent: `<channel chat_id="${chatId}">go</channel>`,
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('P2 / #64: background sub-agent persists across parent turn boundaries', () => {
|
|
68
|
-
it('PerChatState for turn A survives parent turn_end while background fleet member runs', () => {
|
|
69
|
-
const { driver, completions } = harness()
|
|
70
|
-
const CHAT = 'c1'
|
|
71
|
-
|
|
72
|
-
// Turn A
|
|
73
|
-
driver.ingest(enqueue(CHAT, '1'), null)
|
|
74
|
-
driver.ingest(
|
|
75
|
-
{
|
|
76
|
-
kind: 'tool_use',
|
|
77
|
-
toolName: 'Agent',
|
|
78
|
-
toolUseId: 'tu1',
|
|
79
|
-
input: { prompt: 'bg work', description: 'long-bg', run_in_background: true },
|
|
80
|
-
},
|
|
81
|
-
CHAT,
|
|
82
|
-
)
|
|
83
|
-
driver.ingest(
|
|
84
|
-
{ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg work' },
|
|
85
|
-
CHAT,
|
|
86
|
-
)
|
|
87
|
-
// Parent reply fires + delivery so turn_end takes the ✅ Done path.
|
|
88
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
89
|
-
driver.recordOutboundDelivered(CHAT)
|
|
90
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
|
|
91
|
-
|
|
92
|
-
// Background sub-agent is still running → onTurnComplete must NOT
|
|
93
|
-
// have fired for turn A yet. Fleet is still inspectable.
|
|
94
|
-
expect(completions.length).toBe(0)
|
|
95
|
-
const fleetA = driver.peekFleet(CHAT)!
|
|
96
|
-
expect(fleetA.has('saBG')).toBe(true)
|
|
97
|
-
expect(fleetA.get('saBG')!.status).toBe('background')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('background sub-agent tool_use after a NEW turn arrives still updates the originating turn fleet', () => {
|
|
101
|
-
const { driver, completions, advance } = harness()
|
|
102
|
-
const CHAT = 'c1'
|
|
103
|
-
|
|
104
|
-
// Turn A spawns bg sub-agent
|
|
105
|
-
driver.ingest(enqueue(CHAT, '1'), null)
|
|
106
|
-
driver.ingest(
|
|
107
|
-
{
|
|
108
|
-
kind: 'tool_use',
|
|
109
|
-
toolName: 'Agent',
|
|
110
|
-
toolUseId: 'tu1',
|
|
111
|
-
input: { prompt: 'bg work', description: 'long-bg', run_in_background: true },
|
|
112
|
-
},
|
|
113
|
-
CHAT,
|
|
114
|
-
)
|
|
115
|
-
driver.ingest(
|
|
116
|
-
{ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg work' },
|
|
117
|
-
CHAT,
|
|
118
|
-
)
|
|
119
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
120
|
-
driver.recordOutboundDelivered(CHAT)
|
|
121
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
|
|
122
|
-
|
|
123
|
-
const fleetBeforeTurnB = driver.peekFleet(CHAT)!
|
|
124
|
-
const turnAStartedAt = fleetBeforeTurnB.get('saBG')!.startedAt
|
|
125
|
-
|
|
126
|
-
// Advance the clock so the bg sub-agent's later tool_use gets a
|
|
127
|
-
// distinguishable lastActivityAt (proves routing actually mutated
|
|
128
|
-
// the originating member rather than no-oping).
|
|
129
|
-
advance(50)
|
|
130
|
-
|
|
131
|
-
// Turn B starts (and ends quickly, no sub-agents).
|
|
132
|
-
driver.ingest(enqueue(CHAT, '2'), null)
|
|
133
|
-
advance(10)
|
|
134
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
135
|
-
driver.recordOutboundDelivered(CHAT)
|
|
136
|
-
driver.ingest({ kind: 'turn_end', durationMs: 200 }, CHAT)
|
|
137
|
-
|
|
138
|
-
// Background sub-agent emits a tool_use after parent moved on.
|
|
139
|
-
driver.ingest(
|
|
140
|
-
{
|
|
141
|
-
kind: 'sub_agent_tool_use',
|
|
142
|
-
agentId: 'saBG',
|
|
143
|
-
toolUseId: 'bgt1',
|
|
144
|
-
toolName: 'Read',
|
|
145
|
-
input: { file_path: '/tmp/x.txt' },
|
|
146
|
-
},
|
|
147
|
-
CHAT,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
// The bg fleet member's lastActivityAt advanced — proving routing
|
|
151
|
-
// landed on the originating PerChatState rather than dropping the
|
|
152
|
-
// event as a "late event for ended turn".
|
|
153
|
-
// Iterate all fleets — the originating one survives even if peekFleet
|
|
154
|
-
// returns turn B's state.
|
|
155
|
-
// We discover it via a known-stable agentId.
|
|
156
|
-
// Use the test-only carry: peekFleet returns whichever chat:thread
|
|
157
|
-
// matches; but turn B may shadow it. So use the fleet from turn A
|
|
158
|
-
// by looking it up via the driver's introspection — we just call
|
|
159
|
-
// peekFleet(CHAT) and accept that it returns a fleet where saBG
|
|
160
|
-
// either lives (if A is still bound) or doesn't (if B took over).
|
|
161
|
-
// Either way the saBG entry exists somewhere; check it via the
|
|
162
|
-
// dedicated test hook.
|
|
163
|
-
const allLiveBg = driver.peekFleet(CHAT)
|
|
164
|
-
// saBG might live on turn A's fleet which is no longer the
|
|
165
|
-
// currentTurnKey; but the routing must have updated it. We rely on
|
|
166
|
-
// a debug hook to find it across all chats.
|
|
167
|
-
expect(allLiveBg).toBeDefined()
|
|
168
|
-
// Strict: we expect SOMEWHERE in the driver, saBG's lastActivityAt
|
|
169
|
-
// is now newer than turnAStartedAt.
|
|
170
|
-
// Pull via the driver's test hook (added in P2): peekAllFleets.
|
|
171
|
-
const all = (driver as unknown as { peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, { agentId: string; lastActivityAt: number; toolCount: number }> }> })
|
|
172
|
-
.peekAllFleets?.() ?? []
|
|
173
|
-
let found: { lastActivityAt: number; toolCount: number } | undefined
|
|
174
|
-
for (const entry of all) {
|
|
175
|
-
const m = entry.fleet.get('saBG')
|
|
176
|
-
if (m != null) found = m
|
|
177
|
-
}
|
|
178
|
-
expect(found).toBeDefined()
|
|
179
|
-
expect(found!.toolCount).toBe(1)
|
|
180
|
-
expect(found!.lastActivityAt).toBeGreaterThan(turnAStartedAt)
|
|
181
|
-
// Turn B should have completed normally (no bg on it).
|
|
182
|
-
expect(completions.some((k) => k.endsWith(':2'))).toBe(true)
|
|
183
|
-
// Turn A should NOT have completed yet (bg still running).
|
|
184
|
-
expect(completions.some((k) => k.endsWith(':1'))).toBe(false)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('completes the originating turn when the last background sub-agent reaches turn_end', () => {
|
|
188
|
-
const { driver, completions } = harness()
|
|
189
|
-
const CHAT = 'c1'
|
|
190
|
-
driver.ingest(enqueue(CHAT, '1'), null)
|
|
191
|
-
driver.ingest(
|
|
192
|
-
{
|
|
193
|
-
kind: 'tool_use',
|
|
194
|
-
toolName: 'Agent',
|
|
195
|
-
toolUseId: 'tu1',
|
|
196
|
-
input: { prompt: 'bg', run_in_background: true },
|
|
197
|
-
},
|
|
198
|
-
CHAT,
|
|
199
|
-
)
|
|
200
|
-
driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg' }, CHAT)
|
|
201
|
-
driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
|
|
202
|
-
driver.recordOutboundDelivered(CHAT)
|
|
203
|
-
driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
|
|
204
|
-
expect(completions.length).toBe(0)
|
|
205
|
-
|
|
206
|
-
// BG sub-agent eventually finishes.
|
|
207
|
-
driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
|
|
208
|
-
expect(completions.length).toBe(1)
|
|
209
|
-
expect(completions[0]).toMatch(/:1$/)
|
|
210
|
-
})
|
|
211
|
-
})
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — fleet zone caps at 5 visible rows; surplus collapses
|
|
3
|
-
* to "+ N more" footer. Order is most-recent-activity first.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
-
import { renderFleetZone } from '../two-zone-card.js'
|
|
8
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
9
|
-
|
|
10
|
-
function fm(id: string, lastActivityAt: number): FleetMember {
|
|
11
|
-
return {
|
|
12
|
-
agentId: id,
|
|
13
|
-
role: 'role-' + id,
|
|
14
|
-
startedAt: 0,
|
|
15
|
-
toolCount: 1,
|
|
16
|
-
lastActivityAt,
|
|
17
|
-
lastTool: { name: 'Read', sanitisedArg: 'x.ts' },
|
|
18
|
-
status: 'running',
|
|
19
|
-
terminalAt: null,
|
|
20
|
-
errorSeen: false,
|
|
21
|
-
originatingTurnKey: 'k',
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('renderFleetZone cap', () => {
|
|
26
|
-
it('returns empty string for empty fleet', () => {
|
|
27
|
-
expect(renderFleetZone(new Map(), 0)).toBe('')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('renders all rows for fleet ≤ 5', () => {
|
|
31
|
-
const fleet = new Map([
|
|
32
|
-
['a', fm('aaaaaa', 100)],
|
|
33
|
-
['b', fm('bbbbbb', 200)],
|
|
34
|
-
['c', fm('cccccc', 300)],
|
|
35
|
-
])
|
|
36
|
-
const out = renderFleetZone(fleet, 1000)
|
|
37
|
-
expect(out).toContain('FLEET (3)')
|
|
38
|
-
expect(out).toContain('aaaaaa')
|
|
39
|
-
expect(out).toContain('bbbbbb')
|
|
40
|
-
expect(out).toContain('cccccc')
|
|
41
|
-
expect(out).not.toContain('more')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('caps at 5 with "+ N more" footer for fleet > 5, ordered most-recent-first', () => {
|
|
45
|
-
const fleet = new Map<string, FleetMember>()
|
|
46
|
-
for (let i = 0; i < 7; i++) {
|
|
47
|
-
const id = `agent${i}xx`
|
|
48
|
-
fleet.set(id, fm(id, 100 + i))
|
|
49
|
-
}
|
|
50
|
-
const out = renderFleetZone(fleet, 1000)
|
|
51
|
-
expect(out).toContain('FLEET (7)')
|
|
52
|
-
expect(out).toContain('+ 2 more')
|
|
53
|
-
// Most-recent activity (i=6, ts=106) must appear; oldest two (i=0, i=1) must not
|
|
54
|
-
expect(out).toContain('agent6')
|
|
55
|
-
expect(out).toContain('agent2')
|
|
56
|
-
expect(out).not.toContain('agent0')
|
|
57
|
-
expect(out).not.toContain('agent1')
|
|
58
|
-
// Count visible rows by counting status glyphs at row starts
|
|
59
|
-
const rowLines = out.split('\n').filter((l) => l.startsWith('↻'))
|
|
60
|
-
expect(rowLines.length).toBe(5)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — fleet row formatting: id6 truncation, role fallback,
|
|
3
|
-
* terminal status suffix, glyph mapping.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
renderFleetRow,
|
|
9
|
-
glyphForFleetStatus,
|
|
10
|
-
formatRelativeTime,
|
|
11
|
-
} from '../two-zone-card.js'
|
|
12
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
13
|
-
|
|
14
|
-
function fm(over: Partial<FleetMember>): FleetMember {
|
|
15
|
-
return {
|
|
16
|
-
agentId: 'abcdef0123456789',
|
|
17
|
-
role: 'agent',
|
|
18
|
-
startedAt: 0,
|
|
19
|
-
toolCount: 0,
|
|
20
|
-
lastActivityAt: 1000,
|
|
21
|
-
lastTool: null,
|
|
22
|
-
status: 'running',
|
|
23
|
-
terminalAt: null,
|
|
24
|
-
errorSeen: false,
|
|
25
|
-
originatingTurnKey: 'k',
|
|
26
|
-
...over,
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('glyphForFleetStatus', () => {
|
|
31
|
-
it('maps every status to a glyph', () => {
|
|
32
|
-
expect(glyphForFleetStatus('running')).toBe('↻')
|
|
33
|
-
expect(glyphForFleetStatus('background')).toBe('⏸')
|
|
34
|
-
expect(glyphForFleetStatus('done')).toBe('✓')
|
|
35
|
-
expect(glyphForFleetStatus('failed')).toBe('✗')
|
|
36
|
-
expect(glyphForFleetStatus('stuck')).toBe('⚠')
|
|
37
|
-
expect(glyphForFleetStatus('killed')).toBe('✗')
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('formatRelativeTime', () => {
|
|
42
|
-
it('seconds under 60', () => {
|
|
43
|
-
expect(formatRelativeTime(3000)).toBe('3s ago')
|
|
44
|
-
})
|
|
45
|
-
it('minutes + seconds', () => {
|
|
46
|
-
expect(formatRelativeTime(72_000)).toBe('1m12s ago')
|
|
47
|
-
})
|
|
48
|
-
it('zero', () => {
|
|
49
|
-
expect(formatRelativeTime(0)).toBe('0s ago')
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
describe('renderFleetRow', () => {
|
|
54
|
-
const NOW = 10_000
|
|
55
|
-
|
|
56
|
-
it('uses 6-char id slice', () => {
|
|
57
|
-
const out = renderFleetRow(fm({ agentId: 'abcdef0123456789' }), NOW)
|
|
58
|
-
expect(out).toContain('abcdef')
|
|
59
|
-
expect(out).not.toContain('abcdef0')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('renders running with last tool + age', () => {
|
|
63
|
-
const out = renderFleetRow(fm({
|
|
64
|
-
lastActivityAt: NOW - 5000,
|
|
65
|
-
lastTool: { name: 'Read', sanitisedArg: 'file.ts' },
|
|
66
|
-
toolCount: 3,
|
|
67
|
-
}), NOW)
|
|
68
|
-
expect(out).toContain('Read')
|
|
69
|
-
expect(out).toContain('file.ts')
|
|
70
|
-
expect(out).toContain('5s ago')
|
|
71
|
-
expect(out).toContain('3t')
|
|
72
|
-
expect(out.startsWith('↻')).toBe(true)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('renders terminal done with relative time', () => {
|
|
76
|
-
const out = renderFleetRow(fm({
|
|
77
|
-
status: 'done',
|
|
78
|
-
terminalAt: NOW - 12_000,
|
|
79
|
-
lastActivityAt: NOW - 12_000,
|
|
80
|
-
}), NOW)
|
|
81
|
-
expect(out).toContain('done 12s ago')
|
|
82
|
-
expect(out.startsWith('✓')).toBe(true)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('renders failed terminal with status suffix', () => {
|
|
86
|
-
const out = renderFleetRow(fm({
|
|
87
|
-
status: 'failed',
|
|
88
|
-
terminalAt: NOW - 3000,
|
|
89
|
-
lastActivityAt: NOW - 3000,
|
|
90
|
-
errorSeen: true,
|
|
91
|
-
}), NOW)
|
|
92
|
-
expect(out).toContain('failed 3s ago')
|
|
93
|
-
expect(out.startsWith('✗')).toBe(true)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('falls back when no lastTool yet', () => {
|
|
97
|
-
const out = renderFleetRow(fm({ lastActivityAt: NOW }), NOW)
|
|
98
|
-
expect(out).toContain('↻')
|
|
99
|
-
expect(out).toContain('agent')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — phaseFor truth table.
|
|
3
|
-
*
|
|
4
|
-
* Drives the `phaseFor(state, fleet)` resolver across the 6-row spec
|
|
5
|
-
* table from `reference/status-card-design.md` plus edge cases that
|
|
6
|
-
* have historically been mis-classified (parent-stalled-fleet-active,
|
|
7
|
-
* parent-done-fg-failed-bg-running, reply-and-fleet, sub-text-only).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect } from 'vitest'
|
|
11
|
-
import { phaseFor } from '../two-zone-card.js'
|
|
12
|
-
import type { FleetMember } from '../fleet-state.js'
|
|
13
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
14
|
-
|
|
15
|
-
function fm(id: string, status: FleetMember['status'], lastActivityAt: number = 1000): FleetMember {
|
|
16
|
-
return {
|
|
17
|
-
agentId: id,
|
|
18
|
-
role: 'agent',
|
|
19
|
-
startedAt: 0,
|
|
20
|
-
toolCount: 0,
|
|
21
|
-
lastActivityAt,
|
|
22
|
-
lastTool: null,
|
|
23
|
-
status,
|
|
24
|
-
terminalAt: status === 'done' || status === 'failed' || status === 'killed' ? lastActivityAt : null,
|
|
25
|
-
errorSeen: status === 'failed',
|
|
26
|
-
originatingTurnKey: 'k',
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function st(opts: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
|
|
31
|
-
return {
|
|
32
|
-
turnStartedAt: 1,
|
|
33
|
-
items: [],
|
|
34
|
-
narratives: [],
|
|
35
|
-
stage: opts.stage,
|
|
36
|
-
thinking: false,
|
|
37
|
-
subAgents: new Map(),
|
|
38
|
-
pendingAgentSpawns: new Map(),
|
|
39
|
-
tasks: [],
|
|
40
|
-
...opts,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const fleetOf = (...members: FleetMember[]) => new Map(members.map((m) => [m.agentId, m]))
|
|
45
|
-
|
|
46
|
-
const NOW = 100_000
|
|
47
|
-
|
|
48
|
-
describe('phaseFor truth table', () => {
|
|
49
|
-
it.each([
|
|
50
|
-
// [name, state, fleet, opts, expectedLabel]
|
|
51
|
-
['working: parent in flight, no fleet', st({ stage: 'run' }), new Map(), {}, 'Working…'],
|
|
52
|
-
['working: parent in flight + fleet running', st({ stage: 'run' }), fleetOf(fm('a', 'running', NOW)), {}, 'Working…'],
|
|
53
|
-
['background: parent done, bg running', st({ stage: 'done' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
|
|
54
|
-
['background: parentDone flag + fg running', st({ stage: 'run' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
|
|
55
|
-
['stalled: parent idle + all fleet stuck', st({ stage: 'run' }), fleetOf(fm('a', 'stuck', 0), fm('b', 'stuck', 0)), {}, 'Stalled'],
|
|
56
|
-
['done: parent done + all fleet terminal', st({ stage: 'done' }), fleetOf(fm('a', 'done'), fm('b', 'failed')), {}, 'Done'],
|
|
57
|
-
['done: parent done, no fleet', st({ stage: 'done' }), new Map(), {}, 'Done'],
|
|
58
|
-
['silent: parent terminal + no reply', st({ stage: 'done' }), new Map(), { silentEnd: true }, 'Ended without reply'],
|
|
59
|
-
['forced close: stalledClose flag wins', st({ stage: 'done' }), fleetOf(fm('a', 'done')), { stalledClose: true }, 'Forced close'],
|
|
60
|
-
// Edge cases
|
|
61
|
-
['parent-done + fg-failed + bg-running → Background, not Done', st({ stage: 'done' }), fleetOf(fm('a', 'failed'), fm('b', 'running', NOW)), { parentDone: true }, 'Background'],
|
|
62
|
-
['mixed terminal+stuck → not Done', st({ stage: 'run' }), fleetOf(fm('a', 'done'), fm('b', 'stuck', 0)), {}, 'Stalled'],
|
|
63
|
-
['reply tool fired AND fleet running → Background (parentDone)', st({ stage: 'done' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
|
|
64
|
-
// Regression: pre-fix the `[].every(...)` vacuous-truth at
|
|
65
|
-
// two-zone-card.ts fleetAllStuck would mark the fleet stalled the
|
|
66
|
-
// moment the last sub-agent finished while the parent was still
|
|
67
|
-
// running. Plan agents that completed in 2-3min showed ⚠ Stalled
|
|
68
|
-
// on the pinned card until the parent itself wrapped up. Now: zero
|
|
69
|
-
// running-or-stuck members in the fleet means we fall through to
|
|
70
|
-
// the default "Working…" instead.
|
|
71
|
-
['regression: all fleet done + parent still running → Working… (was Stalled)', st({ stage: 'run' }), fleetOf(fm('a', 'done'), fm('b', 'done')), {}, 'Working…'],
|
|
72
|
-
['regression: lone done sub-agent + parent still running → Working…', st({ stage: 'run' }), fleetOf(fm('a', 'done')), {}, 'Working…'],
|
|
73
|
-
['regression: failed-only fleet + parent still running → Working… (was Stalled)', st({ stage: 'run' }), fleetOf(fm('a', 'failed')), {}, 'Working…'],
|
|
74
|
-
])('%s', (_name, state, fleet, opts, expectedLabel) => {
|
|
75
|
-
const phase = phaseFor(state, fleet, NOW, opts as Record<string, unknown>)
|
|
76
|
-
expect(phase.label).toBe(expectedLabel)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* P1 of #662 — render-invariant property tests.
|
|
3
|
-
*
|
|
4
|
-
* For any input state with fleet 0..50 and any tool-arg shape:
|
|
5
|
-
* 1. Output passes a tag-balance validator (no <blockquote> 400s).
|
|
6
|
-
* 2. Output is < 4096 bytes.
|
|
7
|
-
* 3. Idempotent: same inputs → same output.
|
|
8
|
-
*
|
|
9
|
-
* Uses vitest `it.each` with ~30 hand-crafted shapes covering the
|
|
10
|
-
* property surface (per #662 P1 — replaces fast-check).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect } from 'vitest'
|
|
14
|
-
import { renderTwoZoneCard } from '../two-zone-card.js'
|
|
15
|
-
import type { FleetMember, FleetStatus } from '../fleet-state.js'
|
|
16
|
-
import type { ProgressCardState } from '../progress-card.js'
|
|
17
|
-
import { isBalancedHtml } from './html-balanced.js'
|
|
18
|
-
|
|
19
|
-
const baseState: ProgressCardState = {
|
|
20
|
-
turnStartedAt: 1,
|
|
21
|
-
items: [],
|
|
22
|
-
narratives: [],
|
|
23
|
-
stage: 'run',
|
|
24
|
-
thinking: false,
|
|
25
|
-
subAgents: new Map(),
|
|
26
|
-
pendingAgentSpawns: new Map(),
|
|
27
|
-
tasks: [],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeMember(i: number, status: FleetStatus, argShape: string): FleetMember {
|
|
31
|
-
return {
|
|
32
|
-
agentId: `agent-${i.toString().padStart(8, '0')}`,
|
|
33
|
-
role: i % 3 === 0 ? `worker-${i}` : i % 3 === 1 ? 'general-purpose' : 'investigate the auth bug',
|
|
34
|
-
startedAt: 0,
|
|
35
|
-
toolCount: i,
|
|
36
|
-
lastActivityAt: 100 + i,
|
|
37
|
-
lastTool: i === 0 ? null : { name: 'Read', sanitisedArg: argShape },
|
|
38
|
-
status,
|
|
39
|
-
terminalAt: ['done', 'failed', 'killed'].includes(status) ? 100 + i : null,
|
|
40
|
-
errorSeen: status === 'failed',
|
|
41
|
-
originatingTurnKey: 'k',
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const ARG_SHAPES = [
|
|
46
|
-
'',
|
|
47
|
-
'simple.ts',
|
|
48
|
-
'foo.key',
|
|
49
|
-
'a/b/c/very-long-relative-path-that-should-not-explode-the-card.ts',
|
|
50
|
-
'<html-y-arg>',
|
|
51
|
-
'&already-escaped',
|
|
52
|
-
'emoji 🚀 in arg',
|
|
53
|
-
'quotes "and" \'apostrophes\'',
|
|
54
|
-
'[redacted]',
|
|
55
|
-
'\nmultiline\nshould\nflatten',
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
const STATUSES: FleetStatus[] = ['running', 'background', 'done', 'failed', 'stuck', 'killed']
|
|
59
|
-
|
|
60
|
-
const SIZES = [0, 1, 3, 5, 10, 50]
|
|
61
|
-
|
|
62
|
-
const cases: Array<[string, number, string]> = []
|
|
63
|
-
for (const size of SIZES) {
|
|
64
|
-
for (const arg of ARG_SHAPES.slice(0, 3)) {
|
|
65
|
-
cases.push([`size=${size} arg=${JSON.stringify(arg).slice(0, 20)}`, size, arg])
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
describe('two-zone-card render invariants', () => {
|
|
70
|
-
it.each(cases)('%s — balanced HTML, <4096 bytes, idempotent', (_name, size, arg) => {
|
|
71
|
-
const fleet = new Map<string, FleetMember>()
|
|
72
|
-
for (let i = 0; i < size; i++) {
|
|
73
|
-
const status = STATUSES[i % STATUSES.length]
|
|
74
|
-
fleet.set(`a${i}`, makeMember(i, status, arg))
|
|
75
|
-
}
|
|
76
|
-
const out1 = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
|
|
77
|
-
const out2 = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
|
|
78
|
-
const balance = isBalancedHtml(out1)
|
|
79
|
-
expect(balance.balanced, `unbalanced: open=${balance.openTags.join(',')} extra=${balance.extraCloses.join(',')}`).toBe(true)
|
|
80
|
-
expect(out1.length).toBeLessThan(4096)
|
|
81
|
-
expect(out1).toBe(out2)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('handles arg shapes individually with size=5', () => {
|
|
85
|
-
for (const arg of ARG_SHAPES) {
|
|
86
|
-
const fleet = new Map<string, FleetMember>()
|
|
87
|
-
for (let i = 0; i < 5; i++) fleet.set(`a${i}`, makeMember(i, 'running', arg))
|
|
88
|
-
const out = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
|
|
89
|
-
const b = isBalancedHtml(out)
|
|
90
|
-
expect(b.balanced, `unbalanced for arg=${arg}: open=${b.openTags.join(',')}`).toBe(true)
|
|
91
|
-
expect(out.length).toBeLessThan(4096)
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
describe('html-balanced validator self-test', () => {
|
|
97
|
-
it('balanced cases', () => {
|
|
98
|
-
expect(isBalancedHtml('').balanced).toBe(true)
|
|
99
|
-
expect(isBalancedHtml('plain text').balanced).toBe(true)
|
|
100
|
-
expect(isBalancedHtml('<b>bold</b>').balanced).toBe(true)
|
|
101
|
-
expect(isBalancedHtml('<b>bold <i>italic</i></b>').balanced).toBe(true)
|
|
102
|
-
expect(isBalancedHtml('text with <not a tag>').balanced).toBe(true)
|
|
103
|
-
expect(isBalancedHtml('<blockquote>x</blockquote>').balanced).toBe(true)
|
|
104
|
-
})
|
|
105
|
-
it('unbalanced cases', () => {
|
|
106
|
-
expect(isBalancedHtml('<b>open').balanced).toBe(false)
|
|
107
|
-
expect(isBalancedHtml('close</b>').balanced).toBe(false)
|
|
108
|
-
expect(isBalancedHtml('<b><i>x</b></i>').balanced).toBe(false)
|
|
109
|
-
})
|
|
110
|
-
})
|