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,559 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard v3b — active/fallback marking + promote verb tests.
|
|
3
|
-
*
|
|
4
|
-
* Three surfaces under test:
|
|
5
|
-
* 1. encodeCallbackData / parseCallbackData round-trip for the new
|
|
6
|
-
* `account-promote` + `confirm-account-promote` verbs (`apr`/`cpr`).
|
|
7
|
-
* 2. formatQuotaBar — the mini-bar renderer used on the active row.
|
|
8
|
-
* 3. buildDashboardText / buildDashboardKeyboard — verifies the `▶`
|
|
9
|
-
* glyph floats the active account, the "Fallback ↓:" subhead
|
|
10
|
-
* appears when there's a distinguished active row, and that the
|
|
11
|
-
* v3a unmarked layout is preserved when no account claims active
|
|
12
|
-
* (older CLI without primaryForAgents).
|
|
13
|
-
*
|
|
14
|
-
* Pure module — no gateway/Telegram side effects.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, expect, it } from "vitest";
|
|
18
|
-
import {
|
|
19
|
-
encodeCallbackData,
|
|
20
|
-
parseCallbackData,
|
|
21
|
-
formatQuotaBar,
|
|
22
|
-
buildDashboardText,
|
|
23
|
-
buildDashboardKeyboard,
|
|
24
|
-
buildAccountPromoteConfirmKeyboard,
|
|
25
|
-
CALLBACK_BUDGET_BYTES,
|
|
26
|
-
type AccountSummary,
|
|
27
|
-
type DashboardState,
|
|
28
|
-
} from "../auth-dashboard.js";
|
|
29
|
-
|
|
30
|
-
const baseState: Omit<DashboardState, "accounts"> = {
|
|
31
|
-
agent: "clerk",
|
|
32
|
-
bankId: "clerk",
|
|
33
|
-
plan: "max",
|
|
34
|
-
rateLimitTier: "default_claude_max_20x",
|
|
35
|
-
slots: [],
|
|
36
|
-
quotaHot: false,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const acc = (
|
|
40
|
-
label: string,
|
|
41
|
-
overrides: Partial<AccountSummary> = {},
|
|
42
|
-
): AccountSummary => ({
|
|
43
|
-
label,
|
|
44
|
-
health: "healthy",
|
|
45
|
-
enabledHere: true,
|
|
46
|
-
...overrides,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("v3b: account-promote callback round-trip", () => {
|
|
50
|
-
it("encodes and decodes account-promote (verb apr)", () => {
|
|
51
|
-
const encoded = encodeCallbackData({
|
|
52
|
-
kind: "account-promote",
|
|
53
|
-
agent: "clerk",
|
|
54
|
-
label: "pixsoul@gmail.com",
|
|
55
|
-
});
|
|
56
|
-
expect(encoded).toBe("auth:apr:clerk:pixsoul@gmail.com");
|
|
57
|
-
expect(parseCallbackData(encoded)).toEqual({
|
|
58
|
-
kind: "account-promote",
|
|
59
|
-
agent: "clerk",
|
|
60
|
-
label: "pixsoul@gmail.com",
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("encodes and decodes confirm-account-promote (verb cpr)", () => {
|
|
65
|
-
const encoded = encodeCallbackData({
|
|
66
|
-
kind: "confirm-account-promote",
|
|
67
|
-
agent: "clerk",
|
|
68
|
-
label: "me@kenthompson.com.au",
|
|
69
|
-
});
|
|
70
|
-
expect(encoded).toBe("auth:cpr:clerk:me@kenthompson.com.au");
|
|
71
|
-
expect(parseCallbackData(encoded)).toEqual({
|
|
72
|
-
kind: "confirm-account-promote",
|
|
73
|
-
agent: "clerk",
|
|
74
|
-
label: "me@kenthompson.com.au",
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("rejects labels with disallowed characters", () => {
|
|
79
|
-
// `/` is rejected by isSafeAccountLabel — would create on-disk
|
|
80
|
-
// ambiguity under ~/.switchroom/accounts/.
|
|
81
|
-
expect(parseCallbackData("auth:apr:clerk:bad/label")).toEqual({
|
|
82
|
-
kind: "noop",
|
|
83
|
-
});
|
|
84
|
-
// Whitespace, quotes, etc.
|
|
85
|
-
expect(parseCallbackData("auth:apr:clerk:bad label")).toEqual({
|
|
86
|
-
kind: "noop",
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("rejects payloads beyond the 64-byte cap", () => {
|
|
91
|
-
const longLabel = "a".repeat(60);
|
|
92
|
-
const overlong = `auth:cpr:agent:${longLabel}`;
|
|
93
|
-
// sanity — payload exceeds the cap
|
|
94
|
-
expect(Buffer.byteLength(overlong, "utf8")).toBeGreaterThan(
|
|
95
|
-
CALLBACK_BUDGET_BYTES,
|
|
96
|
-
);
|
|
97
|
-
expect(parseCallbackData(overlong)).toEqual({ kind: "noop" });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("rejects empty label segment", () => {
|
|
101
|
-
expect(parseCallbackData("auth:apr:clerk:")).toEqual({ kind: "noop" });
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe("v3b: formatQuotaBar", () => {
|
|
106
|
-
it("renders all-empty for 0%", () => {
|
|
107
|
-
expect(formatQuotaBar(0)).toBe("░░░░░░");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("renders all-full for 100%", () => {
|
|
111
|
-
expect(formatQuotaBar(100)).toBe("██████");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("clamps below full for 99% so the bar reads visibly under the cap", () => {
|
|
115
|
-
// Critical UX point: a 99% account is one bad turn from exhaustion.
|
|
116
|
-
// The bar must NOT show as full. The cell math floors, so 99/100*6
|
|
117
|
-
// = 5.94 → 5 filled cells.
|
|
118
|
-
expect(formatQuotaBar(99)).toBe("█████░");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("scales linearly across the range", () => {
|
|
122
|
-
expect(formatQuotaBar(50)).toBe("███░░░");
|
|
123
|
-
expect(formatQuotaBar(33)).toBe("█░░░░░"); // 33/100*6=1.98 → 1
|
|
124
|
-
expect(formatQuotaBar(17)).toBe("█░░░░░"); // 17/100*6=1.02 → 1
|
|
125
|
-
expect(formatQuotaBar(83)).toBe("████░░"); // 83/100*6=4.98 → 4
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("clamps negative or >100 inputs to the legal range", () => {
|
|
129
|
-
expect(formatQuotaBar(-5)).toBe("░░░░░░");
|
|
130
|
-
expect(formatQuotaBar(150)).toBe("██████");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("supports a custom cell count", () => {
|
|
134
|
-
expect(formatQuotaBar(50, 10)).toBe("█████░░░░░");
|
|
135
|
-
expect(formatQuotaBar(0, 0)).toBe("");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("v3b: buildDashboardText — active-row marking", () => {
|
|
140
|
-
it("floats the activeForThisAgent row to the top with a ▶ glyph", () => {
|
|
141
|
-
const state: DashboardState = {
|
|
142
|
-
...baseState,
|
|
143
|
-
accounts: [
|
|
144
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
145
|
-
acc("me@kenthompson.com.au"),
|
|
146
|
-
acc("ken.thompson@outlook.com.au"),
|
|
147
|
-
],
|
|
148
|
-
};
|
|
149
|
-
const text = buildDashboardText(state);
|
|
150
|
-
const pixIdx = text.indexOf("pixsoul@gmail.com");
|
|
151
|
-
const meIdx = text.indexOf("me@kenthompson.com.au");
|
|
152
|
-
expect(pixIdx).toBeGreaterThan(-1);
|
|
153
|
-
expect(meIdx).toBeGreaterThan(-1);
|
|
154
|
-
// Active row precedes fallbacks in the rendered text.
|
|
155
|
-
expect(pixIdx).toBeLessThan(meIdx);
|
|
156
|
-
// ▶ glyph appears on the active row, before the label.
|
|
157
|
-
const arrowIdx = text.indexOf("▶");
|
|
158
|
-
expect(arrowIdx).toBeGreaterThan(-1);
|
|
159
|
-
expect(arrowIdx).toBeLessThan(pixIdx);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("emits a 'Fallback ↓:' subhead when there's a distinguished active row", () => {
|
|
163
|
-
const state: DashboardState = {
|
|
164
|
-
...baseState,
|
|
165
|
-
accounts: [
|
|
166
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
167
|
-
acc("me@kenthompson.com.au"),
|
|
168
|
-
],
|
|
169
|
-
};
|
|
170
|
-
expect(buildDashboardText(state)).toContain("Fallback");
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("falls back to the v3a unmarked layout when no account claims active", () => {
|
|
174
|
-
// Older CLI without primaryForAgents → activeForThisAgent is unset
|
|
175
|
-
// on every account → no ▶ glyph, no Fallback subhead. The v3a
|
|
176
|
-
// bullet-list rendering still works.
|
|
177
|
-
const state: DashboardState = {
|
|
178
|
-
...baseState,
|
|
179
|
-
accounts: [acc("pixsoul@gmail.com"), acc("me@kenthompson.com.au")],
|
|
180
|
-
};
|
|
181
|
-
const text = buildDashboardText(state);
|
|
182
|
-
expect(text).not.toContain("▶");
|
|
183
|
-
expect(text).not.toContain("Fallback");
|
|
184
|
-
// Both labels still appear.
|
|
185
|
-
expect(text).toContain("pixsoul@gmail.com");
|
|
186
|
-
expect(text).toContain("me@kenthompson.com.au");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("renders inline mini-bars on the active row when both percentages are known", () => {
|
|
190
|
-
const state: DashboardState = {
|
|
191
|
-
...baseState,
|
|
192
|
-
accounts: [
|
|
193
|
-
acc("pixsoul@gmail.com", {
|
|
194
|
-
activeForThisAgent: true,
|
|
195
|
-
fiveHourPct: 47,
|
|
196
|
-
sevenDayPct: 12,
|
|
197
|
-
}),
|
|
198
|
-
],
|
|
199
|
-
};
|
|
200
|
-
const text = buildDashboardText(state);
|
|
201
|
-
// Both bars present (the "█"/"░" cells appear in the active-row's
|
|
202
|
-
// inline summary). Spot-check the 47% → "██░░░░░" (47/100*6=2.82
|
|
203
|
-
// → 2 filled cells) and 12% → "░░░░░░" (12/100*6=0.72 → 0 filled).
|
|
204
|
-
expect(text).toContain(formatQuotaBar(47));
|
|
205
|
-
expect(text).toContain(formatQuotaBar(12));
|
|
206
|
-
expect(text).toContain("47%");
|
|
207
|
-
expect(text).toContain("12%");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("falls back to the legacy quota-line on the active row when only one percentage is known", () => {
|
|
211
|
-
const state: DashboardState = {
|
|
212
|
-
...baseState,
|
|
213
|
-
accounts: [
|
|
214
|
-
acc("pixsoul@gmail.com", {
|
|
215
|
-
activeForThisAgent: true,
|
|
216
|
-
fiveHourPct: 47,
|
|
217
|
-
// sevenDayPct intentionally absent
|
|
218
|
-
}),
|
|
219
|
-
],
|
|
220
|
-
};
|
|
221
|
-
const text = buildDashboardText(state);
|
|
222
|
-
// No mini-bar (would require both); the legacy line shows just 5h.
|
|
223
|
-
expect(text).toContain("47%");
|
|
224
|
-
expect(text).not.toContain("12%");
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("uses the existing 'exhausted · resets in …' line when active is exhausted", () => {
|
|
228
|
-
const state: DashboardState = {
|
|
229
|
-
...baseState,
|
|
230
|
-
accounts: [
|
|
231
|
-
acc("pixsoul@gmail.com", {
|
|
232
|
-
activeForThisAgent: true,
|
|
233
|
-
quotaExhaustedUntil: Date.now() + 90 * 60_000,
|
|
234
|
-
fiveHourPct: 100,
|
|
235
|
-
sevenDayPct: 50,
|
|
236
|
-
}),
|
|
237
|
-
],
|
|
238
|
-
};
|
|
239
|
-
const text = buildDashboardText(state);
|
|
240
|
-
expect(text).toContain("exhausted");
|
|
241
|
-
expect(text).toContain("resets in");
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
describe("v3c: buildDashboardKeyboard — single Switch primary button", () => {
|
|
246
|
-
// v3c replaces the v3b per-fallback `⤴ Promote` flood with a single
|
|
247
|
-
// `🔀 Switch primary →` entry that opens a picker sub-keyboard.
|
|
248
|
-
// Pin the visibility rules + the picker behaviour so a refactor can't
|
|
249
|
-
// silently re-surface the v3b button explosion.
|
|
250
|
-
const renderRows = (
|
|
251
|
-
accounts: AccountSummary[],
|
|
252
|
-
): Array<Array<{ text: string; data: string }>> => {
|
|
253
|
-
const kb = buildDashboardKeyboard({ ...baseState, accounts });
|
|
254
|
-
const raw = (kb as unknown as { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> })
|
|
255
|
-
.inline_keyboard;
|
|
256
|
-
return raw.map((row) =>
|
|
257
|
-
row.map((b) => ({ text: b.text, data: b.callback_data })),
|
|
258
|
-
);
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
it("emits exactly ONE `🔀 Switch primary →` button when fallbacks exist", () => {
|
|
262
|
-
const rows = renderRows([
|
|
263
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
264
|
-
acc("me@kenthompson.com.au"),
|
|
265
|
-
acc("ken.thompson@outlook.com.au"),
|
|
266
|
-
]);
|
|
267
|
-
const switchButtons = rows
|
|
268
|
-
.flat()
|
|
269
|
-
.filter((b) => b.text.includes("Switch primary"));
|
|
270
|
-
expect(switchButtons.length).toBe(1);
|
|
271
|
-
expect(switchButtons[0].data).toBe("auth:spv:clerk");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("hides the Switch primary button when no fallback exists", () => {
|
|
275
|
-
// Only one account, and it's already active → nothing to switch to.
|
|
276
|
-
const rows = renderRows([
|
|
277
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
278
|
-
]);
|
|
279
|
-
const switchButtons = rows
|
|
280
|
-
.flat()
|
|
281
|
-
.filter((b) => b.text.includes("Switch primary"));
|
|
282
|
-
expect(switchButtons.length).toBe(0);
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it("hides the Switch primary button when no account claims active", () => {
|
|
286
|
-
// Older CLI without primaryForAgents → activeForThisAgent unset
|
|
287
|
-
// everywhere → can't tell which account to keep, so no picker.
|
|
288
|
-
const rows = renderRows([
|
|
289
|
-
acc("pixsoul@gmail.com"),
|
|
290
|
-
acc("me@kenthompson.com.au"),
|
|
291
|
-
]);
|
|
292
|
-
const switchButtons = rows
|
|
293
|
-
.flat()
|
|
294
|
-
.filter((b) => b.text.includes("Switch primary"));
|
|
295
|
-
expect(switchButtons.length).toBe(0);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("does NOT emit per-fallback ⤴ Promote buttons on the main board", () => {
|
|
299
|
-
// The whole point of v3c — kill the button flood.
|
|
300
|
-
const rows = renderRows([
|
|
301
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
302
|
-
acc("me@kenthompson.com.au"),
|
|
303
|
-
acc("ken.thompson@outlook.com.au"),
|
|
304
|
-
]);
|
|
305
|
-
const promoteRows = rows.flat().filter((b) => b.text.includes("⤴ Promote"));
|
|
306
|
-
expect(promoteRows.length).toBe(0);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("does NOT emit per-account drilldown buttons on the main board", () => {
|
|
310
|
-
// v3c also drops the per-account `account-view` drilldown buttons
|
|
311
|
-
// (av verb) — the text already names every account, the sub-views
|
|
312
|
-
// are reachable via Switch primary / Reauth / Add buttons.
|
|
313
|
-
const rows = renderRows([
|
|
314
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
315
|
-
acc("me@kenthompson.com.au"),
|
|
316
|
-
]);
|
|
317
|
-
const drilldownRows = rows
|
|
318
|
-
.flat()
|
|
319
|
-
.filter((b) => b.data.startsWith("auth:av:"));
|
|
320
|
-
expect(drilldownRows.length).toBe(0);
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
describe("v3c: buildSwitchPrimaryKeyboard — picker", () => {
|
|
325
|
-
it("emits one row per candidate, each fires confirm-account-promote", async () => {
|
|
326
|
-
const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
|
|
327
|
-
const kb = buildSwitchPrimaryKeyboard("clerk", [
|
|
328
|
-
{ label: "me@kenthompson.com.au", health: "healthy" },
|
|
329
|
-
{ label: "ken.thompson@outlook.com.au", health: "healthy" },
|
|
330
|
-
]);
|
|
331
|
-
const raw = (kb as unknown as {
|
|
332
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
333
|
-
}).inline_keyboard;
|
|
334
|
-
// 2 candidate rows + 1 cancel row.
|
|
335
|
-
expect(raw.length).toBe(3);
|
|
336
|
-
expect(raw[0][0].callback_data).toBe(
|
|
337
|
-
"auth:cpr:clerk:me@kenthompson.com.au",
|
|
338
|
-
);
|
|
339
|
-
expect(raw[1][0].callback_data).toBe(
|
|
340
|
-
"auth:cpr:clerk:ken.thompson@outlook.com.au",
|
|
341
|
-
);
|
|
342
|
-
// Cancel returns to the main board via refresh.
|
|
343
|
-
expect(raw[2][0].text).toContain("Cancel");
|
|
344
|
-
expect(raw[2][0].callback_data).toBe("auth:refresh:clerk");
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("renders a noop fallback when a candidate's payload exceeds 64 bytes", async () => {
|
|
348
|
-
const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
|
|
349
|
-
const kb = buildSwitchPrimaryKeyboard("a".repeat(50), [
|
|
350
|
-
{ label: "b".repeat(50), health: "healthy" },
|
|
351
|
-
]);
|
|
352
|
-
const raw = (kb as unknown as {
|
|
353
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
354
|
-
}).inline_keyboard;
|
|
355
|
-
const guarded = raw[0][0];
|
|
356
|
-
expect(guarded.text).toContain("(use CLI)");
|
|
357
|
-
expect(guarded.callback_data).toBe("auth:noop");
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("appends health suffix to each candidate row", async () => {
|
|
361
|
-
const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
|
|
362
|
-
const kb = buildSwitchPrimaryKeyboard("clerk", [
|
|
363
|
-
{ label: "expired@x.com", health: "expired" },
|
|
364
|
-
{ label: "good@x.com", health: "healthy" },
|
|
365
|
-
]);
|
|
366
|
-
const raw = (kb as unknown as {
|
|
367
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
368
|
-
}).inline_keyboard;
|
|
369
|
-
expect(raw[0][0].text).toContain("⌛");
|
|
370
|
-
expect(raw[1][0].text).not.toContain("⌛");
|
|
371
|
-
expect(raw[1][0].text).not.toContain("⚠");
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe("v3c: switch-primary-view callback round-trip", () => {
|
|
376
|
-
it("encodes and decodes (verb spv)", () => {
|
|
377
|
-
const encoded = encodeCallbackData({
|
|
378
|
-
kind: "switch-primary-view",
|
|
379
|
-
agent: "clerk",
|
|
380
|
-
});
|
|
381
|
-
expect(encoded).toBe("auth:spv:clerk");
|
|
382
|
-
expect(parseCallbackData(encoded)).toEqual({
|
|
383
|
-
kind: "switch-primary-view",
|
|
384
|
-
agent: "clerk",
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("rejects unsafe agent names", () => {
|
|
389
|
-
expect(parseCallbackData("auth:spv:bad/agent")).toEqual({ kind: "noop" });
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
describe("v3b: Slots + Pool sections hide when active-account signal is present", () => {
|
|
394
|
-
// The slot row was rendering `● pixsoul@gmail.com (active) ✓ healthy`
|
|
395
|
-
// when the active label was known — a 1:1 duplicate of the
|
|
396
|
-
// `▶ pixsoul@gmail.com ✓` active-account row above. Same for the
|
|
397
|
-
// `Pool: pixsoul@gmail.com is active` line. So we hide both sections
|
|
398
|
-
// entirely under the new account model. Pin the visibility rules so
|
|
399
|
-
// a refactor can't silently re-surface the duplication.
|
|
400
|
-
const slotRowState = (
|
|
401
|
-
activeAccountLabel: string | null,
|
|
402
|
-
): DashboardState => ({
|
|
403
|
-
...baseState,
|
|
404
|
-
slots: [
|
|
405
|
-
{
|
|
406
|
-
slot: "default",
|
|
407
|
-
active: true,
|
|
408
|
-
health: "active",
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
accounts:
|
|
412
|
-
activeAccountLabel != null
|
|
413
|
-
? [
|
|
414
|
-
acc(activeAccountLabel, { activeForThisAgent: true }),
|
|
415
|
-
acc("ken.thompson@outlook.com.au"),
|
|
416
|
-
]
|
|
417
|
-
: [acc("ken.thompson@outlook.com.au")],
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("hides the Slots section entirely when an active-account signal is present", () => {
|
|
421
|
-
const text = buildDashboardText(slotRowState("pixsoul@gmail.com"));
|
|
422
|
-
// No "Slots (N)" header, no "default" leaking out, no Pool line.
|
|
423
|
-
expect(text).not.toContain("Slots (");
|
|
424
|
-
expect(text).not.toContain("default");
|
|
425
|
-
expect(text).not.toMatch(/Pool:/);
|
|
426
|
-
// The ▶ active row is the single source of truth for what's active.
|
|
427
|
-
expect(text).toContain("▶");
|
|
428
|
-
expect(text).toContain("pixsoul@gmail.com");
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("keeps the legacy Slots + Pool layout when accounts have no active signal", () => {
|
|
432
|
-
// Older CLIs don't emit primaryForAgents → no activeForThisAgent
|
|
433
|
-
// is set on any account → slots section is the only signal of
|
|
434
|
-
// "what's active." Preserve it for graceful degradation.
|
|
435
|
-
const text = buildDashboardText(slotRowState(null));
|
|
436
|
-
expect(text).toContain("Slots (");
|
|
437
|
-
expect(text).toContain("<code>default</code> (active)");
|
|
438
|
-
expect(text).toContain("Pool:");
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("keeps the Slots section visible when no accounts exist (fresh-fleet bootstrap)", () => {
|
|
442
|
-
// Bootstrap path: no accounts yet, the operator's only handle is
|
|
443
|
-
// the slot — they need [➕ Add slot] / [🔄 Reauth] to work.
|
|
444
|
-
const text = buildDashboardText({
|
|
445
|
-
...baseState,
|
|
446
|
-
slots: [{ slot: "default", active: true, health: "active" }],
|
|
447
|
-
accounts: [],
|
|
448
|
-
});
|
|
449
|
-
expect(text).toContain("Slots (");
|
|
450
|
-
expect(text).toContain("default");
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
describe("v3b: buildAccountPromoteConfirmKeyboard", () => {
|
|
455
|
-
it("emits a confirm row whose callback dispatches confirm-account-promote", () => {
|
|
456
|
-
const kb = buildAccountPromoteConfirmKeyboard("clerk", "pixsoul@gmail.com");
|
|
457
|
-
const raw = (kb as unknown as { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> }).inline_keyboard;
|
|
458
|
-
const confirm = raw.flat().find((b) => b.text.includes("Confirm promote"));
|
|
459
|
-
expect(confirm?.callback_data).toBe("auth:cpr:clerk:pixsoul@gmail.com");
|
|
460
|
-
const cancel = raw.flat().find((b) => b.text.includes("Cancel"));
|
|
461
|
-
expect(cancel?.callback_data).toBe("auth:refresh:clerk");
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
describe("regression: button count cap on the main board", () => {
|
|
466
|
-
// Real-world wedge: a screenshot from /auth showed 8 buttons stacked
|
|
467
|
-
// vertically on a three-account fleet (the v3b explosion). v3c
|
|
468
|
-
// collapsed everything into a Switch primary picker. Pin the cap so
|
|
469
|
-
// a future "let's add one more affordance" PR can't bring it back.
|
|
470
|
-
const renderRows = (accounts: AccountSummary[]): number => {
|
|
471
|
-
const kb = buildDashboardKeyboard({ ...baseState, accounts });
|
|
472
|
-
return (
|
|
473
|
-
kb as unknown as {
|
|
474
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
475
|
-
}
|
|
476
|
-
).inline_keyboard.length;
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
it("renders <=6 keyboard rows with three accounts (down from 8 in v3b)", () => {
|
|
480
|
-
// pixsoul (active) + 2 fallbacks. Expected layout:
|
|
481
|
-
// row 1: 🔀 Switch primary →
|
|
482
|
-
// row 2: 🔄 Reauth + ➕ Add slot (2 buttons, 1 row)
|
|
483
|
-
// row 3: 📊 Full quota
|
|
484
|
-
// row 4: 🔁 Refresh
|
|
485
|
-
// = 4 rows. Cap at 6 leaves room for a future row without letting
|
|
486
|
-
// the v3b explosion return.
|
|
487
|
-
expect(
|
|
488
|
-
renderRows([
|
|
489
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
490
|
-
acc("me@kenthompson.com.au"),
|
|
491
|
-
acc("ken.thompson@outlook.com.au"),
|
|
492
|
-
]),
|
|
493
|
-
).toBeLessThanOrEqual(6);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it("never emits a Promote button targeting the active account", () => {
|
|
497
|
-
// The original screenshot bug: ⤴ Promote pixsoul@gmail.com
|
|
498
|
-
// appeared even when pixsoul was the active row. Pin that no
|
|
499
|
-
// promote callback (apr/cpr verbs) targets the active label.
|
|
500
|
-
const kb = buildDashboardKeyboard({
|
|
501
|
-
...baseState,
|
|
502
|
-
accounts: [
|
|
503
|
-
acc("pixsoul@gmail.com", { activeForThisAgent: true }),
|
|
504
|
-
acc("me@kenthompson.com.au"),
|
|
505
|
-
],
|
|
506
|
-
});
|
|
507
|
-
const allButtons = (
|
|
508
|
-
kb as unknown as {
|
|
509
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
510
|
-
}
|
|
511
|
-
).inline_keyboard.flat();
|
|
512
|
-
for (const btn of allButtons) {
|
|
513
|
-
const m = btn.callback_data.match(/^auth:(?:apr|cpr):[^:]+:(.+)$/);
|
|
514
|
-
if (m) {
|
|
515
|
-
expect(m[1], "active label found in promote callback").not.toBe(
|
|
516
|
-
"pixsoul@gmail.com",
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
describe("regression: [⚠️ Fall back now] button stays gone (v0.6.11)", () => {
|
|
524
|
-
// Removed when the Switch primary picker became the operator-facing
|
|
525
|
-
// surface for the same outcome. Two paths to the same action
|
|
526
|
-
// confused operators. If quotaHot ever re-surfaces the button, this
|
|
527
|
-
// test catches it.
|
|
528
|
-
it("absent regardless of quotaHot, slot health, or accounts shape", () => {
|
|
529
|
-
const cases: Array<Parameters<typeof buildDashboardKeyboard>[0]> = [
|
|
530
|
-
{ ...baseState, quotaHot: false },
|
|
531
|
-
{ ...baseState, quotaHot: true },
|
|
532
|
-
{
|
|
533
|
-
...baseState,
|
|
534
|
-
quotaHot: true,
|
|
535
|
-
slots: [{ slot: "default", active: true, health: "quota-exhausted" }],
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
...baseState,
|
|
539
|
-
accounts: [
|
|
540
|
-
acc("pixsoul", { activeForThisAgent: true, fiveHourPct: 99 }),
|
|
541
|
-
],
|
|
542
|
-
},
|
|
543
|
-
];
|
|
544
|
-
for (const state of cases) {
|
|
545
|
-
const kb = buildDashboardKeyboard(state);
|
|
546
|
-
const labels = (
|
|
547
|
-
kb as unknown as {
|
|
548
|
-
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
549
|
-
}
|
|
550
|
-
).inline_keyboard
|
|
551
|
-
.flat()
|
|
552
|
-
.map((b) => b.text);
|
|
553
|
-
expect(
|
|
554
|
-
labels.some((t) => /fall.?back/i.test(t)),
|
|
555
|
-
`Fall back surfaced under quotaHot=${state.quotaHot}, slots=${state.slots?.length}`,
|
|
556
|
-
).toBe(false);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
});
|