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,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* First-paint seam tests — Phase 2 of #545.
|
|
3
|
-
*
|
|
4
|
-
* Drives `firstPaintTurn` directly with fake bot api, fake progress driver,
|
|
5
|
-
* and a stub `controllerFactory`. Measures wall-clock deltas against the
|
|
6
|
-
* spec-doc deadlines (waiting-ux-spec.md):
|
|
7
|
-
*
|
|
8
|
-
* F2 ("instant draft / status reaction"):
|
|
9
|
-
* bot.api.setMessageReaction(... '👀' ...) within 800ms of seam entry.
|
|
10
|
-
* F3 ("progress card start"):
|
|
11
|
-
* progressDriver.startTurn within 800ms of seam entry. (The spec only
|
|
12
|
-
* pins 800ms on the status reaction; we mirror that bound here because
|
|
13
|
-
* progress-card start is a synchronous side effect of the same seam
|
|
14
|
-
* and shares the "first visible signal" contract.)
|
|
15
|
-
*
|
|
16
|
-
* The seam is a pure async fn, so 'within Xms' really means 'before any
|
|
17
|
-
* fake-timer advance' — we assert with the wall clock pinned, then verify
|
|
18
|
-
* elapsed-fake-time stays <800ms.
|
|
19
|
-
*
|
|
20
|
-
* RED-or-GREEN: Phase 2 is allowed to surface real seam bugs. Do NOT alter
|
|
21
|
-
* production code to force these green; if a deadline is missed, that's a
|
|
22
|
-
* bug to file.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
26
|
-
import {
|
|
27
|
-
firstPaintTurn,
|
|
28
|
-
type FirstPaintCtx,
|
|
29
|
-
type FirstPaintDeps,
|
|
30
|
-
} from '../first-paint.js'
|
|
31
|
-
import type { StatusReactionController } from '../status-reactions.js'
|
|
32
|
-
import type { DraftStreamHandle } from '../draft-stream.js'
|
|
33
|
-
|
|
34
|
-
const CHAT = '8248703757'
|
|
35
|
-
const INBOUND_MSG = 100
|
|
36
|
-
const STATUS_REACTION_DEADLINE_MS = 800
|
|
37
|
-
const PROGRESS_CARD_DEADLINE_MS = 800
|
|
38
|
-
|
|
39
|
-
type ReactionCall = {
|
|
40
|
-
chatId: string | number
|
|
41
|
-
messageId: number
|
|
42
|
-
emoji: string
|
|
43
|
-
/** Wall-clock at invocation (Date.now under fake timers). */
|
|
44
|
-
ts: number
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type StartTurnCall = {
|
|
48
|
-
chatId: string
|
|
49
|
-
threadId?: string
|
|
50
|
-
userText: string
|
|
51
|
-
replyToMessageId?: number
|
|
52
|
-
ts: number
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface Harness {
|
|
56
|
-
deps: FirstPaintDeps
|
|
57
|
-
ctx: FirstPaintCtx
|
|
58
|
-
reactionCalls: ReactionCall[]
|
|
59
|
-
startTurnCalls: StartTurnCall[]
|
|
60
|
-
controllerCalls: { setQueued: number; cancel: number }
|
|
61
|
-
errors: string[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function makeHarness(overrides: { ctx?: Partial<FirstPaintCtx> } = {}): Harness {
|
|
65
|
-
const reactionCalls: ReactionCall[] = []
|
|
66
|
-
const startTurnCalls: StartTurnCall[] = []
|
|
67
|
-
const controllerCalls = { setQueued: 0, cancel: 0 }
|
|
68
|
-
const errors: string[] = []
|
|
69
|
-
|
|
70
|
-
const fakeController = {
|
|
71
|
-
setQueued: () => {
|
|
72
|
-
controllerCalls.setQueued += 1
|
|
73
|
-
},
|
|
74
|
-
cancel: () => {
|
|
75
|
-
controllerCalls.cancel += 1
|
|
76
|
-
},
|
|
77
|
-
// Surface enough of the public API to satisfy callers; unused here.
|
|
78
|
-
setThinking: () => {},
|
|
79
|
-
setTool: () => {},
|
|
80
|
-
setCompacting: () => {},
|
|
81
|
-
setDone: () => {},
|
|
82
|
-
setSilent: () => {},
|
|
83
|
-
setError: () => {},
|
|
84
|
-
} as unknown as StatusReactionController
|
|
85
|
-
|
|
86
|
-
const deps: FirstPaintDeps = {
|
|
87
|
-
bot: {
|
|
88
|
-
api: {
|
|
89
|
-
setMessageReaction: async (chatId, messageId, reactions) => {
|
|
90
|
-
for (const r of reactions) {
|
|
91
|
-
reactionCalls.push({
|
|
92
|
-
chatId,
|
|
93
|
-
messageId,
|
|
94
|
-
emoji: r.emoji as string,
|
|
95
|
-
ts: Date.now(),
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
progressDriver: {
|
|
102
|
-
startTurn: (args) => {
|
|
103
|
-
startTurnCalls.push({ ...args, ts: Date.now() })
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
activeStatusReactions: new Map(),
|
|
107
|
-
activeReactionMsgIds: new Map(),
|
|
108
|
-
activeTurnStartedAt: new Map(),
|
|
109
|
-
progressUpdateTurnCount: new Map(),
|
|
110
|
-
activeDraftStreams: new Map<string, DraftStreamHandle>(),
|
|
111
|
-
activeDraftParseModes: new Map(),
|
|
112
|
-
suppressPtyPreview: new Set(),
|
|
113
|
-
statusKey: (chatId, threadId) => `${chatId}:${threadId ?? '_'}`,
|
|
114
|
-
streamKey: (chatId, threadId) => `${chatId}:${threadId ?? '_'}`,
|
|
115
|
-
purgeReactionTracking: () => {},
|
|
116
|
-
signalTracker: {
|
|
117
|
-
noteSignal: () => {},
|
|
118
|
-
reset: () => {},
|
|
119
|
-
},
|
|
120
|
-
resolveAgentDirFromEnv: () => null,
|
|
121
|
-
addActiveReaction: () => {},
|
|
122
|
-
logStreamingEvent: () => {},
|
|
123
|
-
controllerFactory: () => fakeController,
|
|
124
|
-
logError: (m) => {
|
|
125
|
-
errors.push(m)
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const ctx: FirstPaintCtx = {
|
|
130
|
-
chatId: CHAT,
|
|
131
|
-
messageId: INBOUND_MSG,
|
|
132
|
-
messageThreadId: undefined,
|
|
133
|
-
isSteerPrefix: false,
|
|
134
|
-
effectiveText: 'hi',
|
|
135
|
-
inboundReceivedAt: Date.now(),
|
|
136
|
-
access: { statusReactions: true },
|
|
137
|
-
...overrides.ctx,
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return { deps, ctx, reactionCalls, startTurnCalls, controllerCalls, errors }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
beforeEach(() => {
|
|
144
|
-
vi.useFakeTimers()
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
afterEach(() => {
|
|
148
|
-
vi.useRealTimers()
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
describe('firstPaintTurn — first-paint seam', () => {
|
|
152
|
-
it('happy path: fresh turn fires status reaction (👀 via setQueued path) and startTurn in order', async () => {
|
|
153
|
-
const h = makeHarness()
|
|
154
|
-
const t0 = Date.now()
|
|
155
|
-
|
|
156
|
-
await firstPaintTurn(h.deps, h.ctx)
|
|
157
|
-
|
|
158
|
-
// Fresh turn: controllerFactory was used, setQueued called once.
|
|
159
|
-
expect(h.controllerCalls.setQueued).toBe(1)
|
|
160
|
-
expect(h.controllerCalls.cancel).toBe(0)
|
|
161
|
-
|
|
162
|
-
// The fake controller is a stub, so the bot.api.setMessageReaction
|
|
163
|
-
// path here is exercised only via the steer/queued branches — for a
|
|
164
|
-
// fresh turn the seam delegates the actual emoji emission to the
|
|
165
|
-
// controller (which our stub records as `setQueued`). Either way the
|
|
166
|
-
// FIRST visible signal happens within the seam call. Assert startTurn
|
|
167
|
-
// fired and the controller was queued before any timer advance.
|
|
168
|
-
expect(h.startTurnCalls).toHaveLength(1)
|
|
169
|
-
expect(h.startTurnCalls[0].chatId).toBe(CHAT)
|
|
170
|
-
expect(h.startTurnCalls[0].userText).toBe('hi')
|
|
171
|
-
expect(h.startTurnCalls[0].replyToMessageId).toBe(INBOUND_MSG)
|
|
172
|
-
|
|
173
|
-
// No fake-time advance happened; both side effects fired synchronously.
|
|
174
|
-
expect(Date.now() - t0).toBe(0)
|
|
175
|
-
expect(h.errors).toHaveLength(0)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('F2 — instant draft: status reaction fires within 800ms of seam entry (fresh turn)', async () => {
|
|
179
|
-
const h = makeHarness()
|
|
180
|
-
const t0 = Date.now()
|
|
181
|
-
|
|
182
|
-
await firstPaintTurn(h.deps, h.ctx)
|
|
183
|
-
|
|
184
|
-
// The "first visible signal" for a fresh turn is the controller's
|
|
185
|
-
// queued state. Production wires this through the controller's emit
|
|
186
|
-
// callback (which calls bot.api.setMessageReaction with 👀). Our
|
|
187
|
-
// controllerFactory stub records `setQueued` instead — so we assert
|
|
188
|
-
// setQueued landed within the deadline.
|
|
189
|
-
expect(h.controllerCalls.setQueued).toBe(1)
|
|
190
|
-
const elapsed = Date.now() - t0
|
|
191
|
-
expect(elapsed).toBeLessThan(STATUS_REACTION_DEADLINE_MS)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('F2 — instant draft (queued mid-turn branch): 👀 reaction fires synchronously within 800ms', async () => {
|
|
195
|
-
const h = makeHarness()
|
|
196
|
-
// Simulate prior turn in flight by seeding activeStatusReactions.
|
|
197
|
-
const key = `${CHAT}:_`
|
|
198
|
-
const placeholderCtrl = {
|
|
199
|
-
cancel: () => {},
|
|
200
|
-
setQueued: () => {},
|
|
201
|
-
} as unknown as StatusReactionController
|
|
202
|
-
h.deps.activeStatusReactions.set(key, placeholderCtrl)
|
|
203
|
-
h.deps.activeTurnStartedAt.set(key, Date.now() - 5_000)
|
|
204
|
-
|
|
205
|
-
const t0 = Date.now()
|
|
206
|
-
await firstPaintTurn(h.deps, h.ctx)
|
|
207
|
-
|
|
208
|
-
// Mid-turn queued branch posts 👀 directly via bot.api.
|
|
209
|
-
const eyes = h.reactionCalls.find((c) => c.emoji === '👀')
|
|
210
|
-
expect(eyes, 'expected a 👀 reaction call in mid-turn queued branch').toBeDefined()
|
|
211
|
-
expect((eyes!.ts) - t0).toBeLessThan(STATUS_REACTION_DEADLINE_MS)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('F3 — progress card: startTurn fires within 800ms of seam entry on a fresh turn', async () => {
|
|
215
|
-
const h = makeHarness()
|
|
216
|
-
const t0 = Date.now()
|
|
217
|
-
|
|
218
|
-
await firstPaintTurn(h.deps, h.ctx)
|
|
219
|
-
|
|
220
|
-
expect(h.startTurnCalls).toHaveLength(1)
|
|
221
|
-
const elapsed = h.startTurnCalls[0].ts - t0
|
|
222
|
-
expect(elapsed).toBeLessThan(PROGRESS_CARD_DEADLINE_MS)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('F3 — does NOT fire startTurn when a prior turn is in flight (steer branch)', async () => {
|
|
226
|
-
const h = makeHarness({ ctx: { isSteerPrefix: true } })
|
|
227
|
-
const key = `${CHAT}:_`
|
|
228
|
-
const placeholderCtrl = {
|
|
229
|
-
cancel: () => {},
|
|
230
|
-
setQueued: () => {},
|
|
231
|
-
} as unknown as StatusReactionController
|
|
232
|
-
h.deps.activeStatusReactions.set(key, placeholderCtrl)
|
|
233
|
-
h.deps.activeTurnStartedAt.set(key, Date.now() - 5_000)
|
|
234
|
-
|
|
235
|
-
const result = await firstPaintTurn(h.deps, h.ctx)
|
|
236
|
-
|
|
237
|
-
expect(result.isSteering).toBe(true)
|
|
238
|
-
expect(h.startTurnCalls).toHaveLength(0)
|
|
239
|
-
// Steer branch posts 🤝, not a new card.
|
|
240
|
-
expect(h.reactionCalls.find((c) => c.emoji === '🤝')).toBeDefined()
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
it('logError dep captures progress-card startTurn failures (does not write to stderr)', async () => {
|
|
244
|
-
const h = makeHarness()
|
|
245
|
-
h.deps.progressDriver = {
|
|
246
|
-
startTurn: () => {
|
|
247
|
-
throw new Error('boom')
|
|
248
|
-
},
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
await firstPaintTurn(h.deps, h.ctx)
|
|
252
|
-
|
|
253
|
-
expect(h.errors).toHaveLength(1)
|
|
254
|
-
expect(h.errors[0]).toContain('progress-card startTurn failed')
|
|
255
|
-
expect(h.errors[0]).toContain('boom')
|
|
256
|
-
})
|
|
257
|
-
})
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the create-agent flow state machine (foreman-create-flow.ts).
|
|
3
|
-
*
|
|
4
|
-
* Pure function tests — no grammY, no SQLite, no network.
|
|
5
|
-
*
|
|
6
|
-
* Covers:
|
|
7
|
-
* - startCreateFlow: valid/invalid name, inline name, no name
|
|
8
|
-
* - handleFlowText: step transitions (asked-name → asked-profile → asked-bot-token → ...)
|
|
9
|
-
* - Error paths: invalid name, unknown profile, bad token shape, short code
|
|
10
|
-
* - makeInitialState / advanceState / stepLabel helpers
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { describe, it, expect } from 'vitest'
|
|
14
|
-
import {
|
|
15
|
-
startCreateFlow,
|
|
16
|
-
handleFlowText,
|
|
17
|
-
makeInitialState,
|
|
18
|
-
advanceState,
|
|
19
|
-
stepLabel,
|
|
20
|
-
isValidAgentName,
|
|
21
|
-
} from '../foreman/foreman-create-flow.js'
|
|
22
|
-
import type { CreateFlowState } from '../foreman/state.js'
|
|
23
|
-
|
|
24
|
-
const PROFILES = ['default', 'health-coach', 'coding-assistant']
|
|
25
|
-
|
|
26
|
-
// ─── isValidAgentName ─────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
describe('foreman-create-flow: isValidAgentName', () => {
|
|
29
|
-
it('accepts lowercase simple name', () => expect(isValidAgentName('gymbro')).toBe(true))
|
|
30
|
-
it('accepts name with hyphens', () => expect(isValidAgentName('my-agent')).toBe(true))
|
|
31
|
-
it('accepts name with underscores', () => expect(isValidAgentName('my_agent')).toBe(true))
|
|
32
|
-
it('accepts name starting with digit', () => expect(isValidAgentName('1agent')).toBe(true))
|
|
33
|
-
it('rejects uppercase', () => expect(isValidAgentName('Gymbro')).toBe(false))
|
|
34
|
-
it('rejects empty string', () => expect(isValidAgentName('')).toBe(false))
|
|
35
|
-
it('rejects spaces', () => expect(isValidAgentName('my agent')).toBe(false))
|
|
36
|
-
it('rejects semicolon', () => expect(isValidAgentName('agent; evil')).toBe(false))
|
|
37
|
-
it('accepts 51-char name', () => expect(isValidAgentName('a'.repeat(51))).toBe(true))
|
|
38
|
-
it('rejects 52-char name', () => expect(isValidAgentName('a'.repeat(52))).toBe(false))
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
// ─── startCreateFlow ──────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
describe('foreman-create-flow: startCreateFlow', () => {
|
|
44
|
-
it('asks for name when no inline name given', () => {
|
|
45
|
-
const action = startCreateFlow(null, PROFILES)
|
|
46
|
-
expect(action.kind).toBe('ask-name')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('asks for profile when valid inline name given', () => {
|
|
50
|
-
const action = startCreateFlow('gymbro', PROFILES)
|
|
51
|
-
expect(action.kind).toBe('ask-profile')
|
|
52
|
-
if (action.kind === 'ask-profile') {
|
|
53
|
-
expect(action.profiles).toEqual(PROFILES)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('returns error when inline name is invalid', () => {
|
|
58
|
-
const action = startCreateFlow('Bad Name!', PROFILES)
|
|
59
|
-
expect(action.kind).toBe('error')
|
|
60
|
-
if (action.kind === 'error') {
|
|
61
|
-
expect(action.message).toContain('Bad Name!')
|
|
62
|
-
expect(action.stayInStep).toBe(false)
|
|
63
|
-
}
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('returns error for uppercase inline name', () => {
|
|
67
|
-
const action = startCreateFlow('MyAgent', PROFILES)
|
|
68
|
-
expect(action.kind).toBe('error')
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
// ─── handleFlowText — null state ─────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
describe('foreman-create-flow: handleFlowText with null state', () => {
|
|
75
|
-
it('cancels when no active flow', () => {
|
|
76
|
-
const action = handleFlowText({ state: null, text: 'hello', profiles: PROFILES })
|
|
77
|
-
expect(action.kind).toBe('cancel')
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// ─── handleFlowText — asked-name step ────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
describe('foreman-create-flow: handleFlowText step=asked-name', () => {
|
|
84
|
-
function makeState(): CreateFlowState {
|
|
85
|
-
return makeInitialState('chat-1', null) // step = 'asked-name'
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
it('transitions to ask-profile on valid name', () => {
|
|
89
|
-
const action = handleFlowText({ state: makeState(), text: 'gymbro', profiles: PROFILES })
|
|
90
|
-
expect(action.kind).toBe('ask-profile')
|
|
91
|
-
if (action.kind === 'ask-profile') {
|
|
92
|
-
expect(action.profiles).toEqual(PROFILES)
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('returns error on invalid name, stayInStep=true', () => {
|
|
97
|
-
const action = handleFlowText({ state: makeState(), text: 'Bad Name!', profiles: PROFILES })
|
|
98
|
-
expect(action.kind).toBe('error')
|
|
99
|
-
if (action.kind === 'error') {
|
|
100
|
-
expect(action.stayInStep).toBe(true)
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('error message mentions the bad input', () => {
|
|
105
|
-
const action = handleFlowText({ state: makeState(), text: 'MyBotIsGreat', profiles: PROFILES })
|
|
106
|
-
if (action.kind === 'error') {
|
|
107
|
-
expect(action.message).toContain('MyBotIsGreat')
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('accepts name with hyphens', () => {
|
|
112
|
-
const action = handleFlowText({ state: makeState(), text: 'my-agent', profiles: PROFILES })
|
|
113
|
-
expect(action.kind).toBe('ask-profile')
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// ─── handleFlowText — asked-profile step ──────────────────────────────────
|
|
118
|
-
|
|
119
|
-
describe('foreman-create-flow: handleFlowText step=asked-profile', () => {
|
|
120
|
-
function makeState(name = 'gymbro'): CreateFlowState {
|
|
121
|
-
return {
|
|
122
|
-
chatId: 'chat-1',
|
|
123
|
-
step: 'asked-profile',
|
|
124
|
-
name,
|
|
125
|
-
profile: null,
|
|
126
|
-
botToken: null,
|
|
127
|
-
authSessionName: null,
|
|
128
|
-
loginUrl: null,
|
|
129
|
-
startedAt: Date.now(),
|
|
130
|
-
updatedAt: Date.now(),
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
it('transitions to ask-bot-token on valid profile', () => {
|
|
135
|
-
const action = handleFlowText({ state: makeState(), text: 'health-coach', profiles: PROFILES })
|
|
136
|
-
expect(action.kind).toBe('ask-bot-token')
|
|
137
|
-
if (action.kind === 'ask-bot-token') {
|
|
138
|
-
expect(action.profile).toBe('health-coach')
|
|
139
|
-
expect(action.name).toBe('gymbro')
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('returns error on unknown profile, stayInStep=true', () => {
|
|
144
|
-
const action = handleFlowText({ state: makeState(), text: 'nonexistent-profile', profiles: PROFILES })
|
|
145
|
-
expect(action.kind).toBe('error')
|
|
146
|
-
if (action.kind === 'error') {
|
|
147
|
-
expect(action.stayInStep).toBe(true)
|
|
148
|
-
expect(action.message).toContain('nonexistent-profile')
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('lists valid profiles in error message', () => {
|
|
153
|
-
const action = handleFlowText({ state: makeState(), text: 'bad', profiles: PROFILES })
|
|
154
|
-
if (action.kind === 'error') {
|
|
155
|
-
for (const p of PROFILES) {
|
|
156
|
-
expect(action.message).toContain(p)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('cancels with missing-name when state.name is unset (#28 item 1)', () => {
|
|
162
|
-
// Pre-#28 fix this fell back to using the profile name as the agent
|
|
163
|
-
// name. Now we cancel cleanly so the user gets a clear restart
|
|
164
|
-
// signal instead of an agent named "default".
|
|
165
|
-
const stateNoName = {
|
|
166
|
-
chatId: 'chat-1',
|
|
167
|
-
step: 'asked-profile' as const,
|
|
168
|
-
name: null,
|
|
169
|
-
profile: null,
|
|
170
|
-
botToken: null,
|
|
171
|
-
authSessionName: null,
|
|
172
|
-
loginUrl: null,
|
|
173
|
-
startedAt: Date.now(),
|
|
174
|
-
updatedAt: Date.now(),
|
|
175
|
-
}
|
|
176
|
-
const action = handleFlowText({ state: stateNoName, text: 'default', profiles: PROFILES })
|
|
177
|
-
expect(action.kind).toBe('cancel')
|
|
178
|
-
if (action.kind === 'cancel') {
|
|
179
|
-
expect(action.reason).toBe('missing-name')
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
// ─── handleFlowText — asked-bot-token step ───────────────────────────────
|
|
185
|
-
|
|
186
|
-
describe('foreman-create-flow: handleFlowText step=asked-bot-token', () => {
|
|
187
|
-
function makeState(): CreateFlowState {
|
|
188
|
-
return {
|
|
189
|
-
chatId: 'chat-1',
|
|
190
|
-
step: 'asked-bot-token',
|
|
191
|
-
name: 'gymbro',
|
|
192
|
-
profile: 'health-coach',
|
|
193
|
-
botToken: null,
|
|
194
|
-
authSessionName: null,
|
|
195
|
-
loginUrl: null,
|
|
196
|
-
startedAt: Date.now(),
|
|
197
|
-
updatedAt: Date.now(),
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
it('transitions to call-create-agent on token-shaped input', () => {
|
|
202
|
-
const token = '1234567890:AAHaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
|
203
|
-
const action = handleFlowText({ state: makeState(), text: token, profiles: PROFILES })
|
|
204
|
-
expect(action.kind).toBe('call-create-agent')
|
|
205
|
-
if (action.kind === 'call-create-agent') {
|
|
206
|
-
expect(action.botToken).toBe(token)
|
|
207
|
-
expect(action.name).toBe('gymbro')
|
|
208
|
-
expect(action.profile).toBe('health-coach')
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('returns error on token with no colon, stayInStep=true', () => {
|
|
213
|
-
const action = handleFlowText({ state: makeState(), text: 'notavalidtoken', profiles: PROFILES })
|
|
214
|
-
expect(action.kind).toBe('error')
|
|
215
|
-
if (action.kind === 'error') {
|
|
216
|
-
expect(action.stayInStep).toBe(true)
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('returns error on token too short', () => {
|
|
221
|
-
const action = handleFlowText({ state: makeState(), text: 'a:b', profiles: PROFILES })
|
|
222
|
-
expect(action.kind).toBe('error')
|
|
223
|
-
if (action.kind === 'error') {
|
|
224
|
-
expect(action.stayInStep).toBe(true)
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('cancels if name or profile missing in state', () => {
|
|
229
|
-
const state: CreateFlowState = {
|
|
230
|
-
...makeState(),
|
|
231
|
-
name: null,
|
|
232
|
-
}
|
|
233
|
-
const action = handleFlowText({ state, text: '1234567890:AAHsomething', profiles: PROFILES })
|
|
234
|
-
expect(action.kind).toBe('cancel')
|
|
235
|
-
})
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
// ─── handleFlowText — asked-oauth-code step ──────────────────────────────
|
|
239
|
-
|
|
240
|
-
describe('foreman-create-flow: handleFlowText step=asked-oauth-code', () => {
|
|
241
|
-
function makeState(): CreateFlowState {
|
|
242
|
-
return {
|
|
243
|
-
chatId: 'chat-1',
|
|
244
|
-
step: 'asked-oauth-code',
|
|
245
|
-
name: 'gymbro',
|
|
246
|
-
profile: 'health-coach',
|
|
247
|
-
botToken: '1234567890:AAHsomething',
|
|
248
|
-
authSessionName: 'gymbro-auth-123',
|
|
249
|
-
loginUrl: 'https://claude.ai/oauth/authorize?...',
|
|
250
|
-
startedAt: Date.now(),
|
|
251
|
-
updatedAt: Date.now(),
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
it('transitions to call-complete-creation on plausible code', () => {
|
|
256
|
-
const action = handleFlowText({ state: makeState(), text: 'abc12345', profiles: PROFILES })
|
|
257
|
-
expect(action.kind).toBe('call-complete-creation')
|
|
258
|
-
if (action.kind === 'call-complete-creation') {
|
|
259
|
-
expect(action.name).toBe('gymbro')
|
|
260
|
-
expect(action.code).toBe('abc12345')
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('returns error on code that is too short, stayInStep=true', () => {
|
|
265
|
-
const action = handleFlowText({ state: makeState(), text: 'ab', profiles: PROFILES })
|
|
266
|
-
expect(action.kind).toBe('error')
|
|
267
|
-
if (action.kind === 'error') {
|
|
268
|
-
expect(action.stayInStep).toBe(true)
|
|
269
|
-
}
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('cancels if name missing in state', () => {
|
|
273
|
-
const state: CreateFlowState = { ...makeState(), name: null }
|
|
274
|
-
const action = handleFlowText({ state, text: 'abc12345', profiles: PROFILES })
|
|
275
|
-
expect(action.kind).toBe('cancel')
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
// ─── handleFlowText — done step ──────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
describe('foreman-create-flow: handleFlowText step=done', () => {
|
|
282
|
-
it('returns cancel when flow is already done', () => {
|
|
283
|
-
const state: CreateFlowState = {
|
|
284
|
-
chatId: 'chat-1',
|
|
285
|
-
step: 'done',
|
|
286
|
-
name: 'gymbro',
|
|
287
|
-
profile: 'health-coach',
|
|
288
|
-
botToken: null,
|
|
289
|
-
authSessionName: null,
|
|
290
|
-
loginUrl: null,
|
|
291
|
-
startedAt: Date.now(),
|
|
292
|
-
updatedAt: Date.now(),
|
|
293
|
-
}
|
|
294
|
-
const action = handleFlowText({ state, text: 'hello', profiles: PROFILES })
|
|
295
|
-
expect(action.kind).toBe('cancel')
|
|
296
|
-
})
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// ─── makeInitialState ─────────────────────────────────────────────────────
|
|
300
|
-
|
|
301
|
-
describe('foreman-create-flow: makeInitialState', () => {
|
|
302
|
-
it('sets step to asked-name when name is null', () => {
|
|
303
|
-
const state = makeInitialState('chat-1', null)
|
|
304
|
-
expect(state.step).toBe('asked-name')
|
|
305
|
-
expect(state.name).toBeNull()
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
it('sets step to asked-profile when name provided', () => {
|
|
309
|
-
const state = makeInitialState('chat-1', 'gymbro')
|
|
310
|
-
expect(state.step).toBe('asked-profile')
|
|
311
|
-
expect(state.name).toBe('gymbro')
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('sets startedAt and updatedAt', () => {
|
|
315
|
-
const before = Date.now()
|
|
316
|
-
const state = makeInitialState('chat-1', null)
|
|
317
|
-
const after = Date.now()
|
|
318
|
-
expect(state.startedAt).toBeGreaterThanOrEqual(before)
|
|
319
|
-
expect(state.startedAt).toBeLessThanOrEqual(after)
|
|
320
|
-
expect(state.updatedAt).toBe(state.startedAt)
|
|
321
|
-
})
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
// ─── advanceState ─────────────────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
describe('foreman-create-flow: advanceState', () => {
|
|
327
|
-
it('merges updates into state', () => {
|
|
328
|
-
const state = makeInitialState('chat-1', 'gymbro')
|
|
329
|
-
const next = advanceState(state, { step: 'asked-bot-token', profile: 'health-coach' })
|
|
330
|
-
expect(next.step).toBe('asked-bot-token')
|
|
331
|
-
expect(next.profile).toBe('health-coach')
|
|
332
|
-
expect(next.name).toBe('gymbro')
|
|
333
|
-
expect(next.chatId).toBe('chat-1')
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
it('updates updatedAt', () => {
|
|
337
|
-
const state = makeInitialState('chat-1', null)
|
|
338
|
-
const before = Date.now()
|
|
339
|
-
const next = advanceState(state, { step: 'asked-profile', name: 'gymbro' })
|
|
340
|
-
expect(next.updatedAt).toBeGreaterThanOrEqual(before)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('preserves startedAt', () => {
|
|
344
|
-
const state = makeInitialState('chat-1', null)
|
|
345
|
-
const next = advanceState(state, { step: 'asked-profile' })
|
|
346
|
-
expect(next.startedAt).toBe(state.startedAt)
|
|
347
|
-
})
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
// ─── stepLabel ────────────────────────────────────────────────────────────
|
|
351
|
-
|
|
352
|
-
describe('foreman-create-flow: stepLabel', () => {
|
|
353
|
-
it('returns a non-empty string for each step', () => {
|
|
354
|
-
const steps = ['asked-name', 'asked-profile', 'asked-bot-token', 'asked-oauth-code', 'done'] as const
|
|
355
|
-
for (const step of steps) {
|
|
356
|
-
expect(stepLabel(step).length).toBeGreaterThan(0)
|
|
357
|
-
}
|
|
358
|
-
})
|
|
359
|
-
})
|