AgentDeck · Continuous integration
Test Report
The most recent CI run, in full: which tests passed, which user scenarios are covered, how the numbers are trending, and where the gaps are. This is the maintainer's evidence that the build works — not a product analytics dashboard.
LATEST RUN · PASS · GENERATED 2026-08-20 11:20:54
Status
PASS
3620 executed tests
Passed
3618
99.9% pass rate
Failed
0
Duration
38.5s
31.9s vitest + 6.6s android
TS Line Coverage
55.1%
14,474/26,257 TypeScript lines
Total Tests
3620
Pass Rate
99.9%
Line Coverage
55.1%
◈
Terminal Output Parsing
PASS
Does the bridge interpret Claude Code and Codex terminal output correctly?
264 passed
0 failed
3 files
259ms
output-parser.test.ts
220
/
220
201ms
OutputParser
220 / 220
basic option parsing › parses clean numbered options with newlines
basic option parsing › detects (recommended) marker
basic option parsing › detects ✔ selected marker
basic option parsing › handles both recommended and selected on different options
basic option parsing › parses full Claude model selector with labels and middle dot
ANSI-stripped options › parses concatenated text with · delimiter
ANSI-stripped options › extracts version numbers from · delimited labels
chunked input debounce › does NOT emit immediately on first chunk
chunked input debounce › batches multiple chunks into single emission
chunked input debounce › resets debounce timer when new chunk with options arrives
chunked input debounce › simulates real /model flow: split across chunks
model ID date rejection › does NOT parse 20251001 from model ID as option number
model ID date rejection › rejects large numbers that look like dates
version number rejection › does NOT parse version "4.6" followed by digit as option
version number rejection › does NOT parse "6)" from "4.6)" as option 6
stale buffer overwrite › newer options overwrite older ones with same index
permission prompts › detects Yes/No/Always pattern
permission prompts › keeps ALL real options when a numbered prompt contains "Yes, allow once" (no collapse to 3)
permission prompts › detects (Y)es/(N)o pattern
permission prompts › emits immediately with no debounce
permission prompts › sets yes_no_always prompt type
permission prompts › sets yes_no prompt type
permission prompts › extracts the multi-line question header above yes/no/always options
permission prompts › extracts the question from a box-framed permission prompt
permission prompts › extracts the inline question from the (Y)es/(N)o line
permission prompts › does not mistake box-drawing or footer chrome for a question
permission prompts › suppresses false idle from user prompt echo after permission (interactive cooldown)
permission prompts › allows real idle after interactive cooldown expires
diff prompts › detects (V)iew/(A)pply/(D)eny pattern
diff prompts › detects lowercase (a)pply/(d)eny/(v)iew pattern
diff prompts › emits immediately with no debounce
diff prompts › suppresses false idle from user prompt echo after diff (interactive cooldown)
various option counts › handles 1 option
various option counts › handles 2 options
various option counts › handles 5+ options
various option counts › marks direct-input options as freeform instead of a normal choice
various option counts › handles bullet-style options
option updates › emits new options when data changes after first emission
spinner cancels option timer › spinner cancels pending option debounce
idle vs option debounce › idle prompt is ignored when option debounce is pending
idle vs option debounce › idle prompt fires normally when no option debounce is pending
interactive prompt during spinner › parses a navigable prompt when spinner and prompt share the first chunk
interactive prompt during spinner › keeps a visible prompt active through spinner redraw floods
interactive prompt during spinner › allows real processing immediately after the prompt is selected
interactive prompt during spinner › does not arm prompt lifecycle from stale numbered prose
interactive prompt during spinner › stops spinner when permission prompt arrives
interactive prompt during spinner › stops spinner when diff prompt arrives
interactive prompt during spinner › stops spinner when option prompt arrives
interactive prompt during spinner › stops spinner when idle prompt arrives in small chunk
interactive prompt during spinner › ignores idle prompt in large chunk during spinner (screen redraw)
cleanOptionLabel (via parseOptions) › deduplicates exact CamelCase matches
cleanOptionLabel (via parseOptions) › preserves labels without · delimiter
cleanOptionLabel (via parseOptions) › removes (recommended) from label text
cleanOptionLabel (via parseOptions) › removes ✔ from label text
cleanOptionLabel (via parseOptions) › extracts version from identity before middle dot
metadata events › emits project_name from startup banner
metadata events › emits project_name only once (caches)
metadata events › parses absolute path for project_name
metadata events › seedProjectName disables the scrape (resolver wins over basename)
metadata events › seeded name survives reset(); scrape stays disabled
metadata events › seeding 'unknown' keeps the scrape live as fallback
metadata events › emits model_info with model and plan
metadata events › emits model_info without plan
metadata events › emits model_info only once for same model (caches)
metadata events › re-emits model_info when model changes
metadata events › parses ANSI-stripped model_info
metadata events › detects API billing plan
metadata events › emits status_line with duration and tokens
metadata events › parses zero-minute status line
metadata events › emits tool_action from ⏺ pattern
metadata events › extracts various tool names
user prompt › emits user_prompt after first idle has been seen
user prompt › does NOT emit user_prompt before first idle
user prompt › filters out mode banner text
user prompt › filters out numbered option lines
user prompt › filters out keyboard hint text
user prompt › filters out "esc to interrupt" text
user prompt › filters out autocomplete suggestions
user prompt › filters out box-drawing decorative lines
usage info › parses usage percentage
usage info › parses usage cost
usage info › parses session percentage with hour limit
usage info › parses reset time with timezone
usage info › parses time remaining
spinner lifecycle › emits spinner_start on spinner char (after idle seen)
spinner lifecycle › does NOT emit spinner_start before first idle
spinner lifecycle › emits spinner_stop after debounce (2000ms)
spinner lifecycle › does NOT emit duplicate spinner_start on repeated chars
spinner lifecycle › resets spinner debounce timer on repeated chars
spinner lifecycle › ignores spinner chars in large text blocks (>80 non-ws)
spinner lifecycle › recognizes all spinner characters
idle detection › emits idle after IDLE_DEBOUNCE_MS (300ms)
idle detection › sets seenFirstIdle on first idle prompt
idle detection › cancels idle timer when spinner starts
idle detection › recognizes > as idle prompt char
mode detection › detects plan mode
mode detection › detects accept edits mode
mode detection › detects default mode after Shift+Tab
mode detection › detects default mode via idle prompt after Shift+Tab
mode detection › emits default mode on Shift+Tab timeout (2s)
mode detection › detects ANSI-stripped plan mode text
mode detection › detects ANSI-stripped accept edits text
mode detection › clears pending mode switch when mode banner is detected
reset › clears cached project name and model name
reset › disables spinner detection (clears seenFirstIdle)
reset › allows re-detection after new idle prompt post-reset
reset › allows re-detection of project name after reset
reset › allows re-detection of model info after reset
buffer management › truncates buffer when exceeding 8192 chars
buffer management › keeps the last 4096 chars after truncation
option navigation › cursor movement during option selection does NOT trigger idle
option navigation › cursor movement does NOT trigger user_prompt
option navigation › normal cursor movement between options emits no idle or user_prompt
option navigation › real idle prompt still works after option navigation
option navigation › effort/hint text during navigation does not trigger events
option navigation › IDLE_PROMPT requires space/tab/NBSP after ❯ (not newline)
option navigation › USER_PROMPT requires space/tab after ❯ (not newline)
permission reclassification › reclassifies numbered Yes/No options as permission_prompt
permission reclassification › does NOT reclassify regular options as permission
permission reclassification › does NOT reclassify cursor-selection UI with "Enter to confirm" as permission
permission reclassification › detects cursor-selection UI even when ANSI stripping removes spaces
permission reclassification › includes navigable and cursorIndex in reclassified permission_prompt
permission reclassification › infers shortcuts for reclassified permission options
permission reclassification › infers shortcut "a" for "don't ask again" labels
permission reclassification › infers shortcut "a" for "allow all sessions" labels
ghost text suggestion › detects SGR 90 (bright black) ghost text on prompt line
ghost text suggestion › detects ghost text via Strategy 1 (Try "..." in clean text)
ghost text suggestion › unwraps Try "..." wrapper with smart quotes
ghost text suggestion › unwraps Try "..." wrapper with straight quotes
ghost text suggestion › detects ghost text on first idle (❯ and suggestion in same chunk)
ghost text suggestion › clears suggestion on spinner start
ghost text suggestion › debounces rapid updates
ghost text suggestion › ignores dim (SGR 2) text without ❯ prompt context
ghost text suggestion › detects dim (SGR 2) ghost text on ❯ prompt line
ghost text suggestion › detects ghost text with cursor-forward spacing (Strategy 1)
ghost text suggestion › filters UI chrome fragments
ghost text suggestion › filters box-drawing lines as UI chrome
ghost text suggestion › filters file paths (false positive from screen redraws)
ghost text suggestion › detects 256-color gray ghost text
ghost text suggestion › handles multi-segment ghost text on prompt line
ghost text suggestion › ignores ghost text on non-prompt lines
ghost text suggestion › detects 24-bit RGB gray ghost text
ghost text suggestion › ignores 24-bit RGB non-gray colors (e.g. blue prompt char)
ghost text suggestion › filters out short gray text like prompt char itself
ghost text suggestion › suppresses ghost text detection during spinner (processing)
ghost text suggestion › rejects digit+operator fragments like diff markers "96 +"
ghost text UI chrome filtering › filters out "Tip:" segments on prompt line
ghost text UI chrome filtering › filters out "(ctrl+...)" shortcut hints on prompt line
ghost text UI chrome filtering › filters concatenated UI chrome even in scheduleSuggestion
ghost text UI chrome filtering › keeps ghost text when UI chrome is also present on the line
filters out extended thinking indicators › rejects "(thought for 1s)" as ghost text via isUiChrome
filters out extended thinking indicators › rejects "(thought for 1s)>" via scheduleSuggestion
filters out extended thinking indicators › rejects longer thinking durations like "(thought for 15s)"
filters out extended thinking indicators › rejects "(thought for 1m 30s)" multipart duration
filters out extended thinking indicators › rejects "✻ Cooked for 1m 26s" sparkle indicator
filters out extended thinking indicators › rejects "✻ Cooked for 5s" short duration variant
filters out extended thinking indicators › rejects "Cooked for 10s" without sparkle (cross-chunk)
parenthesized placeholder filtering › rejects "(no content)" via SGR 2 (dim)
parenthesized placeholder filtering › rejects "(no content)" via SGR 90 (bright black)
parenthesized placeholder filtering › rejects "(loading...)"
parenthesized placeholder filtering › rejects "(empty)"
parenthesized placeholder filtering › rejects "(waiting for response)"
parenthesized placeholder filtering › allows "fix the broken (auth) module" — parens in middle
parenthesized placeholder filtering › allows "(optional) refactor the code" — paren prefix with text after
stacked ANSI + cross-chunk ghost text › detects ghost text with stacked ANSI escapes (gray + italic)
stacked ANSI + cross-chunk ghost text › detects ghost text with combined SGR params (2;90 = dim+bright-black)
stacked ANSI + cross-chunk ghost text › detects cross-chunk ghost text (❯ and gray text in separate feeds)
stacked ANSI + cross-chunk ghost text › does NOT cross-chunk detect when chunk contains ⎿ output fence
stacked ANSI + cross-chunk ghost text › does NOT cross-chunk detect when new chunk has \n (different line)
ghost option from stale buffer › excludes stale numbered list items before actual option prompt
ghost option from stale buffer › bypasses chunk size guard when ❯ cursor is present in large chunk
ghost option from stale buffer › a stale markdown "1." line from chat must not overwrite the ❯-marked option
ghost option from stale buffer › still works with scrambled TUI order after backward scan
ghost option from stale buffer › excludes file path fragments from Read() tool in permission prompt
option index ordering › returns options sorted by index even when TUI lines arrive out of order
split ANSI sequence buffering › handles ANSI SGR codes split across chunks
split ANSI sequence buffering › handles bare ESC at end of chunk
split ANSI sequence buffering › does not buffer complete ANSI sequences
large chunk guard for option detection › does NOT detect numbered items in large response chunks as options
large chunk guard for option detection › still detects real options in small TUI chunks
CUP-positioned options › parses options using CUP (\x1b[row;colH) instead of newlines
CUP-positioned options › parses options using CUD (\x1b[B) for vertical movement
trailing TUI chrome stripping › strips "Esc to cancel" from last option label
trailing TUI chrome stripping › strips "Enter to confirm" from last option label
trailing TUI chrome stripping › does NOT strip "Esc" when it appears within legitimate label text
CJK suggestion detection › accepts ghost text containing CJK characters
CJK suggestion detection › accepts ghost text that is purely CJK (no ASCII words)
AskUserQuestion with separators and descriptions › handles AskUserQuestion with separator and descriptions
AskUserQuestion with separators and descriptions › handles options starting from non-zero index (buffer truncation)
AskUserQuestion tall prompt with oversized separator › keeps leading options a/b when a giant box-drawing rule contaminates a later option
hierarchical CC prompt shapes › captures 5-option plan approval with long labels
hierarchical CC prompt shapes › captures OpenClaw scope selection (numbered list)
hierarchical CC prompt shapes › captures OpenClaw token-action step (short numbered list)
permission scroll does not trigger idle (Bug 1) › scroll chunk with ❯ option text does NOT cause idle when navigable
permission scroll does not trigger idle (Bug 1) › /model combined chunk: confirmation + idle prompt clears options
permission scroll does not trigger idle (Bug 1) › /model separate chunks: ANSI reposition timer does not block idle
permission scroll does not trigger idle (Bug 1) › cursor move chunk with option text does NOT falsely trigger idle
permission scroll does not trigger idle (Bug 1) › genuine idle prompt exits navigable state and emits idle
TUI cursor-overwrite label correction (Bug 2) › fixes contaminated permission option label using correction line
TUI cursor-overwrite label correction (Bug 2) › leaves labels unchanged when no correction line is present
genuine idle detection (semantic) › "❯ \n" is genuine idle — only prompt character, no label text
genuine idle detection (semantic) › "❯ Beta" is NOT idle — has label text after prompt char
genuine idle detection (semantic) › "❯ A" is NOT idle — single-char label still counts
genuine idle detection (semantic) › ">" also treated as idle prompt character
effort level parsing › detects "High effort" selection pattern
effort level parsing › detects "Medium effort" selection pattern
effort level parsing › detects "Low effort" selection pattern
effort level parsing › detects "with high effort" confirmation line
effort level parsing › detects effort in model info line
effort level parsing › caches effort level — no re-emit on same value
effort level parsing › emits on effort level change
effort level parsing › resets effort level on reset()
effort level parsing › does not match "effort" in unrelated context
effort level parsing › does not match effort inside numbered option lines
effort level parsing › getter returns current effort level
effort level parsing › detects "Max effort" (Opus 4.7 variant)
effort level parsing › detects "xhigh effort" (Opus 4.7 variant)
effort level parsing › detects "default effort" (per-model default variant)
effort level parsing › detects "fast effort" (Opus 4.6 variant)
effort level parsing › detects effort in /model confirmation line with max
codex-output-parser.test.ts
29
/
29
32ms
CodexOutputParser
29 / 29
idle detection › emits idle on ❯ prompt with source=prompt
idle detection › emits idle on > prompt
idle detection › debounces rapid idle signals
spinner detection › emits spinner_start on braille spinner chars after first idle
spinner detection › does not emit spinner_start before first idle
spinner detection › emits spinner_stop + idle on timeout with source=timeout
spinner detection › emits spinner_stop when idle prompt appears
spinner detection › detects "Thinking" text as processing
permission prompt detection › emits permission_prompt on Allow/Deny pattern
permission prompt detection › emits permission_prompt on y/n pattern
permission prompt detection › stops spinner when approval detected
permission prompt detection › extracts Allow once / Always allow options
permission prompt detection › detects the current Codex command approval prompt as awaiting permission
tool action detection › detects Running: command pattern
tool action detection › detects file operation patterns
tool action detection › detects Editing file pattern
tool action detection › dedups repeated emits of the same (tool, args) within window
tool action detection › emits a different command immediately
tool action detection › re-emits the same command after the dedup window elapses
tool action detection › re-emits the same command after a turn boundary (idle resets dedup)
model info detection › detects gpt model name
model info detection › detects o-series model
model info detection › does not re-emit same model
project name detection › detects working directory
project name detection › detects project from path
project name detection › only detects project name once
buffer management › truncates buffer at 8192 chars
buffer management › handles incomplete ANSI sequences
getProjectName › returns null when no project detected
cursor-sync.test.ts
15
/
15
25ms
Cursor Synchronization
15 / 15
terminal keyboard cursor tracking › emits cursor_update when full option redraw arrives with ❯ at new position
terminal keyboard cursor tracking › triggers buffer re-parse on small non-❯ chunk during navigable state
terminal keyboard cursor tracking › does not emit cursor_update when cursor position unchanged
genuine idle distinction › treats "❯ \n" as genuine idle (clears navigable state)
genuine idle distinction › does NOT treat "❯ Beta" as idle (cursor on option label)
genuine idle distinction › does NOT treat "❯ Allow once" as idle (permission cursor move)
cursor authority in StateMachine › accepts optimistic update immediately
cursor authority in StateMachine › suppresses stale PTY value within 200ms of optimistic update
cursor authority in StateMachine › accepts PTY value after 200ms grace period
cursor authority in StateMachine › always accepts PTY when no recent optimistic update
cursor authority in StateMachine › resets authority on state transition out of AWAITING
cursor authority in StateMachine › default source parameter is pty
select_option proportional delay › delay scales with step count
option re-emission with cursorIndex › AWAITING_OPTION update triggers state_changed with options
option re-emission with cursorIndex › updateCursorIndex emits snapshot with new cursor value
◇
State Machine & Adapters
PASS
Are agent state transitions and type-specific command routes correct?
196 passed
0 failed
3 files
180ms
state-machine.test.ts
76
/
76
80ms
StateMachine
76 / 76
basic transitions › starts in DISCONNECTED
basic transitions › SessionStart → IDLE
basic transitions › UserPromptSubmit → PROCESSING
basic transitions › Stop → IDLE from PROCESSING
basic transitions › SessionEnd → DISCONNECTED from any state
permission flow › permission_prompt → AWAITING_PERMISSION
permission flow › user respond → PROCESSING from AWAITING_PERMISSION
terminal UI authority boundary › accepts a real interaction affordance
terminal UI authority boundary › ignores terminal lifecycle guesses
option flow › option_prompt → AWAITING_OPTION
option flow › select_option → PROCESSING from AWAITING_OPTION
option flow › option_prompt carries the question into the snapshot
diff flow › diff_prompt → AWAITING_DIFF
diff flow › respond → PROCESSING from AWAITING_DIFF
interrupt › interrupt → IDLE from PROCESSING
interrupt › interrupt → IDLE from AWAITING_PERMISSION
strict transitions › allows IDLE → AWAITING_PERMISSION (prompt without spinner)
strict transitions › blocks invalid transition: DISCONNECTED → PROCESSING
strict transitions › allows wildcard session_end from any state
stuck timeout › PROCESSING for >5min → auto-recovery to IDLE
stuck timeout › AWAITING_PERMISSION persists indefinitely — a genuine wait is never auto-recovered
stuck timeout › AWAITING_OPTION persists indefinitely (no wall-clock recovery)
stuck timeout › AWAITING_DIFF persists indefinitely (no wall-clock recovery)
stuck timeout › AWAITING leaves only on a real signal — parser spinner (OpenCode/OpenClaw adapters) → PROCESSING
stuck timeout › timer resets on state change before timeout
stuck timeout › no timeout in IDLE state
hook exits from awaiting states › Stop from AWAITING_PERMISSION → IDLE with prompt fields cleared
hook exits from awaiting states › Stop from AWAITING_OPTION → IDLE (keyboard-answered AskUserQuestion ends the turn)
hook exits from awaiting states › Stop from AWAITING_DIFF → IDLE
hook exits from awaiting states › UserPromptSubmit from AWAITING_PERMISSION → PROCESSING (new turn dismisses the prompt)
hook exits from awaiting states › PostToolUse after the grace period dismisses AWAITING_PERMISSION → PROCESSING
hook exits from awaiting states › PreToolUse after the grace period dismisses AWAITING_OPTION → PROCESSING
hook exits from awaiting states › tool activity within the grace period keeps a fresh prompt awaiting
hook exits from awaiting states › PreToolUse from IDLE recovers a dropped user_prompt_submit → PROCESSING
hook exits from awaiting states › codex_turn_complete from AWAITING_OPTION → IDLE (notify closes a keyboard-answered prompt)
hook exits from awaiting states › codex_tool_end after the grace period dismisses AWAITING_PERMISSION → PROCESSING
hook exits from awaiting states › a parallel sibling finishing does not dismiss the prompt while the gated tool is in flight
hook exits from awaiting states › a straggler PostToolUse landing after Stop does not reopen a finished session
hook exits from awaiting states › a re-detected prompt refreshes the dismissal grace
hook exits from awaiting states › the daemon hub machine never dismisses prompts or reopens IDLE on tool activity
parser events › spinner_start → PROCESSING from IDLE
parser events › spinner_stop → IDLE from PROCESSING
parser events › idle → IDLE from PROCESSING
parser events › mode_change updates permission mode
snapshot › emits state_changed on transitions
snapshot › includes tool info in snapshot
snapshot › clears tool info on PostToolUse
snapshot › includes project name and model
billingType detection › defaults to unknown
billingType detection › detects subscription from "Claude Max" plan
billingType detection › detects subscription from "Max" (case-insensitive)
billingType detection › detects api from "api.anthropic.com"
billingType detection › detects api (case-insensitive)
billingType detection › stays unknown for unrecognized plan
billingType detection › stays unknown when plan is absent
billingType detection › persists billingType across subsequent model_info without plan
billingType detection › emits state_changed when billingType is set
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_OPTION to PROCESSING
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_PERMISSION to PROCESSING
spinner_start recovery from awaiting states › spinner_start transitions from AWAITING_DIFF to PROCESSING
spinner_start recovery from awaiting states › clears options and navigable state on spinner_start from AWAITING_OPTION
spinner_start recovery from awaiting states › stores navigable and cursorIndex from permission_prompt
spinner_start recovery from awaiting states › cursor index tracking via updateCursorIndex
cursor authority (optimistic vs pty) › optimistic source updates cursor immediately
cursor authority (optimistic vs pty) › suppresses stale PTY within 200ms of optimistic
cursor authority (optimistic vs pty) › accepts PTY after 200ms grace period
cursor authority (optimistic vs pty) › emits state_changed on optimistic update
cursor authority (optimistic vs pty) › does NOT emit state_changed when stale PTY is suppressed
codex_* hook events › codex_session_start → IDLE
codex_* hook events › codex_user_prompt_submit → PROCESSING
codex_* hook events › codex_tool_start sets currentTool from tool_name
codex_* hook events › codex_tool_end clears currentTool
codex_* hook events › codex_stop → IDLE from PROCESSING
codex_* hook events › codex_turn_complete closes PROCESSING when codex_stop is absent
codex_* hook events › codex_turn_complete remains idempotent after codex_stop
codex_* hook events › full codex lifecycle preserves state transitions
adapter.test.ts
95
/
95
66ms
createAdapter factory
5 / 5
creates ClaudeCodeAdapter for "claude-code"
creates OpenClawAdapter for "openclaw"
passes gatewayUrl to OpenClawAdapter
creates CodexCliAdapter for "codex-cli"
throws for unknown agent type
ClaudeCodeAdapter
17 / 17
capabilities › reports all Claude Code capabilities as true
handleCommand routing › handles respond → returns true
handleCommand routing › handles switch_mode → returns true
handleCommand routing › handles interrupt → returns true
handleCommand routing › handles escape → returns true
handleCommand routing › defers select_option to bridge → returns false
handleCommand routing › defers navigate_option to bridge → returns false
handleCommand routing › defers send_prompt to bridge → returns false
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
switch_mode debounce › debounces rapid switch_mode calls (< 100ms apart)
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined before start
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
onRawData callback › can register callback without error
onDiag handler › can register handler without error
OpenClawAdapter
17 / 17
capabilities › reports OpenClaw capabilities correctly
handleCommand routing › handles respond → returns true (RPC)
handleCommand routing › handles select_option → returns true (RPC)
handleCommand routing › handles navigate_option → returns true (no-op)
handleCommand routing › handles send_prompt → returns false without session key
handleCommand routing › handles interrupt → returns true (RPC)
handleCommand routing › handles escape → returns true (RPC)
handleCommand routing › defers switch_mode → returns false (not supported)
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined (no PTY)
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
lifecycle › attachTerminal is a no-op
onRawData callback › can register callback without error
onDiag handler › can register handler without error
OpenClawAdapter gateway protocol
13 / 13
sends connect request with correct format on connect.challenge
becomes alive after hello-ok response
emits spinner_start on chat delta event
emits idle on chat final event
emits idle on chat aborted event
emits idle on chat error event
emits permission_prompt on exec.approval.requested
sends exec.approval.resolve on respond after approval request
sends exec.approval.resolve with deny on respond "n"
sends chat.send on send_prompt with active session
sends chat.abort on interrupt with active session and runId
emits SessionEnd on shutdown event
clears pendingApprovalId on exec.approval.resolved
CodexCliAdapter
16 / 16
capabilities › reports Codex CLI capabilities correctly
handleCommand routing › handles respond → returns true
handleCommand routing › handles interrupt → returns true
handleCommand routing › handles escape → returns true
handleCommand routing › does not handle switch_mode → returns false
handleCommand routing › defers select_option to bridge → returns false
handleCommand routing › defers navigate_option to bridge → returns false
handleCommand routing › defers send_prompt to bridge → returns false
handleCommand routing › defers voice to bridge → returns false
handleCommand routing › defers query_usage to bridge → returns false
lifecycle › isAlive returns false before start
lifecycle › getTtyPath returns undefined before start
lifecycle › getProjectName returns null before start
lifecycle › getHttpServer returns an object
onRawData callback › can register callback without error
onDiag handler › can register handler without error
CodexCliAdapter start lifecycle
3 / 3
emits SessionStart and connected on start
feeds PTY data to output parser and emits activity
exports only terminal UI affordances, not parser lifecycle guesses
MonitorAdapter
18 / 18
capabilities › reports monitor capabilities correctly
handleCommand routing › rejects respond (no PTY)
handleCommand routing › rejects interrupt (no PTY)
handleCommand routing › rejects escape (no PTY)
handleCommand routing › rejects switch_mode (no PTY)
handleCommand routing › rejects select_option (no PTY)
handleCommand routing › rejects send_prompt (no PTY)
handleCommand routing › defers voice to bridge
handleCommand routing › defers query_usage to bridge
lifecycle › isAlive always returns true
lifecycle › getTtyPath returns undefined (no PTY)
lifecycle › getProjectName returns null
lifecycle › getHttpServer returns an object
lifecycle › writeInput is a no-op (no PTY)
lifecycle › attachTerminal is a no-op
lifecycle › onRawData is a no-op
start and event emission › emits connected event on start
start and event emission › exposes hook server for external wiring
ClaudeCodeAdapter start lifecycle
6 / 6
emits SessionStart and connected on start
uses claude as default command
feeds PTY data to output parser and emits activity
exports terminal UI detail without exporting lifecycle or tool guesses
emits SessionEnd and disconnected on PTY exit
registers rawData callback without error
protocol-contract.test.ts
25
/
25
33ms
Protocol Contract — StateUpdateEvent
5 / 5
minimal state_update has required fields
full state_update has all optional fields as correct types
state_update serializes to valid JSON
state enum values are lowercase strings
promptType values match known set
Protocol Contract — UsageEvent
3 / 3
has all required numeric fields
rate limit fields are present when available
tokenStatus matches known values
Protocol Contract — SessionsListEvent
2 / 2
sessions have required fields
agentType is optional string
Protocol Contract — ButtonStateEvent
1 / 1
buttons have required fields
Protocol Contract — EncoderStateEvent
1 / 1
encoders have required fields
Protocol Contract — TimelineEventMsg
1 / 1
entry has required fields
Protocol Contract — ConnectionEvent
1 / 1
has required fields
Protocol Contract — DisplayStateEvent
1 / 1
has boolean displayOn
Protocol Contract — PluginCommand shapes
6 / 6
respond command has value
select_option has numeric index
navigate_option has direction
send_prompt has text
utility command has action
clear_session_focus is fieldless
Protocol Contract — BridgeEvent discriminated union
2 / 2
all event types in union have type field
SERIAL_FORWARDED_EVENTS covers expected types
Protocol Contract — Backward Compatibility
2 / 2
state_update can be parsed with only required fields (old client)
usage_update required fields are sufficient for display
◆
Timeline Pipeline
PASS
Are timeline storage, deduplication, and cross-session relays correct?
125 passed
0 failed
3 files
1.3s
timeline.test.ts
78
/
78
32ms
cleanDetailText
12 / 12
returns empty/falsy input unchanged
strips markdown bold
strips markdown headings
strips code fences
strips inline backticks
strips markdown links
strips blockquotes
strips list markers
collapses multiple blank lines
filters system JSON blobs (connectionId)
extracts error from JSON blob
compacts other JSON
cleanRawText
2 / 2
strips bold, headings, links, backticks
returns empty/falsy unchanged
cleanNopMarkers
3 / 3
removes NOP markers
collapses resulting blank lines
returns empty/falsy unchanged
normalizeTimelineEntryForStorage
12 / 12
summarizes OpenClaw cron model_call prompts instead of storing shell-like instructions
drops low-signal OpenClaw placeholder tool rows
keeps OpenClaw placeholder tool rows when detail carries input or output
keeps coalesced Codex subagent lifecycle rows
drops OpenClaw NO_REPLY polling responses from the user-facing timeline
drops OpenClaw automated polling chat starts from the user-facing timeline
drops Claude task notification chat starts from the user-facing timeline
keeps OpenClaw LINE userId notification failures visible
drops Codex command tool rows even when detail carries input
drops OpenCode tool rows so a tool-heavy turn does not flood the strip
drops Antigravity tool rows too (forward-compat with the observed classifier)
extracts readable labels from cron headers
isAssistantProgressUpdate
2 / 2
detects non-terminal build/status updates
does not classify final completion reports as progress
extractSemanticCore
4 / 4
strips duration suffix for chat_end
keeps full text for chat_end without separator
keeps full text for non-chat_end types
trims whitespace
isRepetitiveEntry
12 / 12
detects exact duplicate chat_end entries
detects keyword-similar entries
returns -1 for non-matching entries
ignores entries outside window
only applies to chat_end, chat_start, and error types
dedupes repeated error entries within 1h window
matches chat_start entries
dedupes automated entries regardless of content
exempts error rows from the content-agnostic automated collapse
does not dedup automated vs non-automated
uses 8h window for automated entries
expires automated dedup after 8h
parseLogLine
21 / 21
returns null for null/undefined/non-object
returns null for empty message
returns null for model start/complete (suppressed)
parses memory/recall (legacy structured)
parses tool execution (legacy structured)
filters gateway/ws subsystem
filters infrastructure noise
filters channel infra reconnect noise
filters connection status JSON blobs
filters transient fetch timeouts
filters edit mismatch errors (agent retries)
filters failover cascade noise
parses genuine errors
parses error from message pattern (not level)
filters model/inference patterns (suppressed)
does not synthesize memory_recall from message text alone
does not synthesize tool_exec from message text alone
parses ISO timestamp correctly via error pattern
falls back to Date.now() for invalid timestamp on error message
truncates long tool raw to 500 chars
filters whatsapp noise from channel infra subsystem
parseSlashCommandPrompt
6 / 6
parses a bare command
parses a command with arguments
parses plugin-scoped command names
parses the Claude Code command XML envelope
normalizes an envelope name missing the leading slash
rejects ordinary prompts and filesystem paths
normalizeCommandPrompt
2 / 2
collapses an XML envelope to "/name args"
passes ordinary prompts through unchanged
deduplicateEntry — interrupted close + cross-session guards
2 / 2
never repetitive-merges rows from different sessions
adds interrupted chat_end rows instead of merging them by semantic core
timeline-integration.test.ts
42
/
42
38ms
BridgeTimelineStore
18 / 18
adds and retrieves entries
filters history by timestamp
returns timeline history sorted by timestamp after late upserts
returns per-session history sorted by timestamp after late upserts
normalizes OpenClaw cron prompt dumps before storing history
drops low-signal OpenClaw placeholder tool rows before broadcast/history
drops Codex tool_exec rows before broadcast/history
loads persisted timeline history without replaying Codex tool_exec firehose
calls listeners on new entries
upsert updates existing entry with same ts+type
upsert keeps timeline attribution fields
upsert adds new entry when no match exists
task_end updates in place by taskId and carries judge fields
updateEntryStatus updates approval status
getLastEntry returns most recent of given type
getLastEntry returns null when type not found
removeListener stops notifications
enforces MAX_ENTRIES (200) with FIFO
deduplicateEntry pipeline
5 / 5
exact duplicate within 8s → skip
same type, different content → add
different type, same raw → add
chat_response identical raw 6s apart → skip (multi-producer race)
chat_response near-duplicate beyond 8s → repetitive merge
BridgeTimelineStore.setAttributor — history replay attribution
10 / 10
addEntry passes the entry through the attributor before storage
caller-set fields take precedence over attributor (idempotent)
listener (broadcast) receives the same attributed entry
upsertEntry propagates summaryKind to existing entry (LLM enrichment regression)
upsertEntry routes through the attributor too
late upsert preserves the original entry attribution after task rotation (regression)
merge path (repetitive dedup) does not re-attribute after task rotation
upsert with no existing match falls through to attributor (insert path)
caller-set taskId on upsert overrides existing entry attribution
history replay returns entries with taskId/runId set (regression)
completion multi-producer double-emit (regression)
1 / 1
identical chat_response from two emit paths is collapsed by store
Timeline → WS broadcast pipeline
4 / 4
store entries trigger broadcast via listener
upsert entries broadcast with upsert flag
exact duplicate within 5s is skipped by store
different content within 5s is NOT deduped
TimelineEntry types
4 / 4
common entry types have expected shape
entries with status field
entries with detail field
entries with automated flag
session-timeline-relay.test.ts
5
/
5
1.2s
SessionTimelineRelay
5 / 5
connects to sibling and relays timeline_event
relays timeline_history entries
removes subscription when session disappears
ignores daemon sessions
handles upsert timeline events
◉
Daemon & Infrastructure
PASS
Are the daemon singleton, session registry, and usage relay stable?
88 passed
0 failed
4 files
3.5s
daemon-lifecycle.test.ts
21
/
21
49ms
daemon.json lifecycle
8 / 8
writeDaemonInfo creates daemon.json with correct content
readDaemonInfo returns info when PID is alive
readDaemonInfo returns null and removes file when PID is dead
readDaemonInfo returns null when file does not exist
removeDaemonInfo deletes daemon.json
removeDaemonInfo is safe when file already gone
self-heals daemon.json after an external deletion
repairs a malformed daemon.json without rewriting a healthy record
session registry with real files
7 / 7
register creates sessions.json
register replaces existing entry with same id
deregister removes session by id
listActive prunes dead PIDs
multiple sessions coexist
findExistingDaemon returns daemon session
findExistingDaemon returns null when no daemon
findDaemonPort
4 / 4
returns port from daemon.json (priority 1)
falls back to sessions.json daemon entry
returns null when no daemon anywhere
daemon.json takes precedence over sessions.json
probeDaemonHealth
2 / 2
returns health JSON from running server
returns null for closed port
session-registry.test.ts
28
/
28
2.1s
Session Registry Logic
24 / 24
pruneDeadSessions › keeps alive sessions
pruneDeadSessions › removes sessions with dead PIDs
pruneDeadSessions › keeps old sessions if PID is alive
pruneDeadSessions › handles mix of alive and dead sessions
port allocation logic › returns base port when no ports are used
port allocation logic › returns next port when base is taken
port allocation logic › skips used ports
port allocation logic › finds gaps in used ports
port allocation logic › throws when all ports are taken
atomic write › write-then-rename produces valid JSON
shouldConcedePortToOccupant (startup-race hardening) › does not concede to a non-daemon occupant (session bridge)
shouldConcedePortToOccupant (startup-race hardening) › does not concede when the probe failed (null occupant)
shouldConcedePortToOccupant (startup-race hardening) › concedes to a daemon backed by a live, distinct PID
shouldConcedePortToOccupant (startup-race hardening) › does NOT concede to a forged/stale daemon whose PID is dead
shouldConcedePortToOccupant (startup-race hardening) › does NOT concede when the reported PID is our own
shouldConcedePortToOccupant (startup-race hardening) › trusts the mode when no PID is reported (e.g. Swift App Store daemon)
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › finds a Swift daemon sitting on a fallback port inside the window
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › ignores non-daemon occupants (session bridges) and skipped ports
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › returns empty when the window is quiet
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › waitForDaemonExit resolves true once /health stops answering
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › waitForDaemonExit resolves false when the daemon never leaves
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › waitForPortBindable refuses a port that is still bound
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › waitForPortBindable resolves true once the socket is actually released
scanDaemonPortWindow / waitForDaemonExit (split-brain guard) › waitForPortBindable resolves true immediately for a free port
daemon port window
4 / 4
defaults when unset or blank
accepts a well-formed range
falls back to the default for anything it cannot trust
reports whether the resolved window is the documented one
usage-relay.test.ts
14
/
14
891ms
Usage relay — HTTP (Tier 1)
3 / 3
fetches usage from sibling GET /usage
returns null usage when sibling has no data
rejects stale data (>5 min old)
Usage relay — WebSocket (Tier 2)
2 / 2
receives usage_update from sibling WS on connect
does not send usage_update when sibling has no data
Usage relay — scoped limits round-trip
2 / 2
preserves scopedLimits (active + severity) over the sibling WS relay
carries scopedLimits through the HTTP /usage relay tier
HookServer GET /usage (relay source)
2 / 2
returns cached usage via onApiUsage getter
returns fresh usage after update
429 prevention — relay-first strategy
2 / 2
sibling with data prevents direct API call
multiple siblings — first with data wins
ApiUsageData shape
3 / 3
has all required fields
serializes to valid JSON and back
handles null optional fields
bridge-core.test.ts
25
/
25
509ms
BridgeCore Orchestration
25 / 25
buildStateEvent › builds state_update with IDLE state
buildStateEvent › includes cached ollamaStatus and gatewayAvailable
buildStateEvent › computes promptType for permission options
buildStateEvent › computes promptType yes_no_always for 3+ permission options
usage management › updateApiUsage caches and broadcasts
usage management › buildUsage includes API usage data
usage management › buildUsage works without API data
usage management › inferredBillingType propagates to StateMachine
usage management › stale fetch results › a not-fresh result keeps the numbers but marks them stale
usage management › stale fetch results › a not-fresh result does NOT advance lastApiFetchTime
usage management › stale fetch results › a fresh result clears staleness again
usage management › stale fetch results › a null result (no numbers at all) marks an existing cache stale
usage management › stale fetch results › reports freshness back to the caller
sendInitialState › sends state_update, usage_update, connection, display_state on connect
sendInitialState › includes timeline_history when entries exist
sendInitialState › caps initial timeline_history below the ESP32 WebSocket frame limit
state change broadcast › state_changed emits to WS clients when wired by caller
wireTimeline › timeline entries broadcast as timeline_event
hasClients guard › wsServer.getClientCount reflects connected clients
hasClients guard › external client count provider extends hasClients
voice assistant state › updateVoiceAssistantState caches and triggers state broadcast
voice assistant state › disabled voice assistant state is not included in event
session registry › registerSession writes to sessions.json
session registry › deregisterSession removes from sessions.json
broadcast coordination › broadcast sends to WS and SSE callback
◎
Integration Tests
PASS
Does the end-to-end pipeline work in a real server environment?
35 passed
0 failed
2 files
1.5s
server-integration.test.ts
19
/
19
1.2s
Server Integration
19 / 19
SessionStart hook → IDLE state broadcast
PreToolUse responds 200 with an EMPTY body (managed sessions must not gate)
non-PreToolUse hooks still ack with { received: true }
full session lifecycle: SessionStart → UserPromptSubmit → Stop
PreToolUse → PostToolUse cycle broadcasts tool info
SessionEnd → DISCONNECTED
GET /health returns valid JSON
GET /usage returns data when getter is set
GET /usage returns null when no getter
GET /devices returns empty when no getter
POST to unknown route returns 404
hook endpoint responds immediately
SSE client receives state_update events
broadcasts to multiple WS clients
WS command from client triggers callback
rapid hook events do not corrupt state
token counts accumulate through hook events
client_register {clientType:"tui"} appears in getTuiClients until the WS closes
malformed tui client_register falls back to a "terminal" identity
tier3-integration.test.ts
16
/
16
265ms
mDNS crash recovery
3 / 3
invalidateMdnsInstance does not throw when no instance exists
isNonFatalMdnsError matches recoverable mDNS multicast failures
isNonFatalMdnsError does NOT match unrelated fatal errors
Display sleep/wake broadcast
7 / 7
DisplayMonitor initial state is ON
parses macOS lock/session presence from ioreg output
treats an absent ScreenIsLocked key as UNLOCKED when IOConsoleUsers is visible
display_state event has correct shape
display_state broadcasts to WS clients
display_state broadcasts to multiple clients
SSE also receives display_state
Voice transcription endpoint
4 / 4
returns 503 when no voice manager is set
returns 400 for empty audio data
returns transcription from voice manager
returns 500 when voice manager throws
WsServer broadcast hooks (serial relay)
2 / 2
onBroadcast hook receives all broadcast events
broadcast hook errors do not crash server
▣
Stream Deck Plugin UI
PASS
Are plugin connections, option layouts, and renderers correct?
63 passed
0 failed
3 files
1.7s
connection-manager.test.ts
17
/
17
17ms
ConnectionManager
17 / 17
starts disconnected
start() begins bridge connection to daemon
start() installs a port provider so daemon.json is re-read every attempt
emits connected when bridge connects
emits disconnected when bridge disconnects
forwards state_update from bridge
send() delegates to bridge
send() drops command when not connected
getBridgePort returns bridge port
getConnectionSnapshot exposes connection and discovery state
retryNow probes daemon discovery and reconnects immediately when disconnected
switchToOpenClaw() › sends switch_agent command to bridge
switchToClaude() › sends switch_agent command to bridge
findDaemonPort() candidate fallthrough › prefers the CLI daemon when it is healthy
findDaemonPort() candidate fallthrough › falls through to the Swift daemon after the CLI port fails
findDaemonPort() candidate fallthrough › reports the daemon as found even when every candidate has failed
findDaemonPort() candidate fallthrough › reports missing when no daemon.json exists
connection-integration.test.ts
6
/
6
1.7s
ConnectionManager Integration — Real WebSocket
6 / 6
client connects and receives broadcast events
multiple clients receive same broadcast
client sends command to server
handles rapid connect/disconnect without crashes
server detects client disconnect
bridge → gateway priority: second server takes over on disconnect
renderer-snapshots.test.ts
40
/
40
31ms
utility-renderer snapshots
2 / 2
renderUtilityGeneric with icon
renderUtilityGeneric muted (text-only value)
launcher-renderer snapshots
3 / 3
renderLauncher first entry
renderLauncher last entry
renderLauncherEmpty
usage-dial-renderer snapshots
1 / 1
renderUsageSession
usage-gauge snapshots
7 / 7
renderUsageGauge Claude 5h (green ramp, low usage)
renderUsageGauge Claude 7d
renderUsageGauge Codex 5h uses the blue brand logo + amber ramp
renderUsageGauge Codex 7d
renderUsageGauge critical (>80 used → red ramp)
renderUsageGauge full-bleed fill (no narrow 60px tank rect)
renderUsageGauge unknown draws a dim tile + dash, no fill
usage-encoder level-fill (SD+ 200x100) snapshots
7 / 7
both-view Claude shows both windows + terracotta brand logo
both-view Codex uses the blue brand logo + severity ramp
single 5h view enlarges the 5H window across the LCD
single 7d view enlarges the 7D window across the LCD
note suppresses the gauges (No Codex usage)
Waiting note before the first usage payload
a single unknown window draws a dash panel, the other a real fill
session-slot-renderer snapshots
10 / 10
disconnected hero is icon-rich
disconnected non-center slot is empty
disconnected cluster quadrant tl
disconnected cluster quadrant tr
disconnected cluster quadrant bl
disconnected cluster quadrant br
connected no-session card is icon-rich
active idle session uses orbiting focus border
stale session dims the render and shows a STALE badge
wraps names with larger typography on original Stream Deck keys
display-tile snapshots
6 / 6
renderStatusReadout MODEL is flat (no raised bezel, no glyph)
renderStatusReadout READY (label only)
renderStatusReadout AWAITING
renderStatusReadout HUB READY (no-session hub state)
renderSessionReadout keeps name/model/state, flat & non-interactive
renderSessionReadout openclaw hides model, shows STANDBY
agent-logos snapshots
4 / 4
claude-code watermark
openclaw watermark
antigravity icon uses the full-color mark
CLAUDE_LOGO_PATH is defined
▤
TUI Dashboard
PASS
Does the terminal dashboard render state and terrarium motion correctly?
53 passed
0 failed
3 files
75ms
tui-dashboard.test.ts
10
/
10
24ms
TUI dashboard models
10 / 10
stores modelCatalog from state_update
renders OAuth catalog and Ollama models in wide layout
renders disconnected OAuth state
renders downstream module health compactly
renders Stream Deck, WiFi-only ESP32, and TUI dashboard downstream rows
shows current session summary and control hints
renders agent list secondary line as model dash compact state
renders sibling models in session bridge mode and omits uptime label
renders help overlay when helpVisible is on
shows numbered session badges
tui-renderer-snapshots.test.ts
29
/
29
18ms
blockGauge snapshots
7 / 7
0% — all empty, green
50% — half filled, green
75% — yellow threshold
95% — red threshold
100% — all filled, red
clamps negative to 0
clamps above 100
resetTimeStr
5 / 5
returns empty for undefined
returns ↻now for past time
formats minutes
formats hours and minutes
formats days and hours
formatUptime
3 / 3
seconds
minutes
hours and minutes
formatTokens
3 / 3
below 1000 unchanged
1k-10k with decimal
10k+ rounded
activityDensityBar
3 / 3
empty timestamps — all dim
recent burst — bright right side
spread timestamps — distributed density
getLayout
3 / 3
wide for 120+ cols
standard for 80-119 cols
narrow for <80 cols
shouldShowTerrarium
3 / 3
true for adequate size
false for too narrow
false for too short
spinner
2 / 2
returns braille characters at different frames
spinner cycles through 10 frames
tui-terrarium-snapshots.test.ts
14
/
14
33ms
TUI terrarium snapshots
14 / 14
initTerrarium creates context with expected structure
setOctopi configures octopus instances
draws an octopus for Claude and for nobody else
setCrayfish configures crayfish state
setJellyfish configures jellyfish instances
renderTerrariumFrame empty terrarium (small)
renderTerrariumFrame empty terrarium (large)
renderTerrariumFrame with idle octopus
renderTerrariumFrame with processing octopus
renderTerrariumFrame with routing crayfish
renderTerrariumFrame with sick crayfish
renderTerrariumFrame with OpenCode hollow ring
renderTerrariumFrame too small returns empty
updateTerrarium advances bubble positions
▥
Serial Protocol
PASS
Is the ESP32 serial byte stream framed correctly?
69 passed
0 failed
1 files
50ms
esp32-serial-node.test.ts
69
/
69
50ms
SERIAL_FORWARDED_EVENTS
4 / 4
includes all display events
includes timeline events (unique to serial)
does NOT include plugin-only events
required serial events are present
ESP32 port detection patterns
5 / 5
matches CH340 ports (86 Box)
matches native USB JTAG ports (IPS 3.5", Round AMOLED)
matches Linux serial ports
excludes Bluetooth and WLAN
excludes non-ESP32 ports
handleSerialLine (source)
7 / 7
parses device_info message and updates deviceInfo
skips debug lines (non-JSON)
opens a panic capture window on crash markers and captures the dump
closes the panic window after expiry and caps captured lines
recovers from malformed JSON
ignores JSON without type field
does not mark deviceInfo present for non-identifying messages
budgetTimelineEntries
2 / 2
keeps the newest entries under the byte budget, in chronological order
passes small histories through untouched
serial I/O error classification
2 / 2
treats nonblocking backpressure as retryable
does not retry terminal serial errors
prepareForSerial (source)
14 / 14
strips agentCapabilities from state_update
strips billingType and remoteUrl from state_update
preserves essential state_update fields
strips bulky state_update fields before device_info arrives
trims sessions_list to firmware fields and string sizes
forwards per-session D1 mosaic fields (tool/elapsed/awaiting prompt) and trims oversized strings
round-robins sessions_list by agent type so non-Claude agents survive the cap
roundRobinByAgentType keeps every present agent type and is a no-op under the cap
strips legacy usage fields from usage_update
preserves Codex window freshness for firmware-side hiding
passes through other events unchanged
caps timeline raw by UTF-8 BYTES on a code-point boundary (한글-safe)
forwards daemon-computed lastEvent* fields byte-capped, omitted when absent
caps timeline_history to the firmware ring (64) AND the line-buffer byte budget
WiFi provision protocol
5 / 5
provision message has all required fields
auto-provisions first-setup boards and refreshes IPS10 endpoint while online
re-arms the pairing token on boards the WiFi gate skips
costs one write per board per token, not one per device_info frame
never writes a credential to an unidentified or empty peer
serial buffer management
3 / 3
line splitting handles multiple lines in one chunk
handles partial message across chunks
truncates buffer when exceeding 8KB limit
foreign port detection
3 / 3
flags a port that opened but never sent valid JSON
does NOT flag a connection that ever read a valid AgentDeck message
does NOT flag a connection that already identified its board
foreign port denylist threshold
2 / 2
denylists a port only after repeated failed probes
treats an elapsed cooldown as no longer denylisted
half-open identified CDC recovery
5 / 5
flags an identified CDC port that has never read since (re)connect
does NOT flag while the read pipe is alive
does NOT flag inside the grace window after connect
does NOT flag an unidentified CDC port — that is the foreign path
does NOT flag a UART (non-CDC) port — handled by the read-timeout branch
serial keepalive ack contract
2 / 2
contains the quoted "keepalive" substring the firmware strstr-matches
is the exact frame the deployed firmware acks
half-open identified UART recovery
6 / 6
flags an identified UART whose RX died mid-connection
does NOT flag while reads are current
does NOT flag below the silence timeout (one lost device_info is not death)
does NOT flag a never-read UART — that is the initial-read/foreign path
does NOT flag an unidentified UART
does NOT flag a CDC port — handled by isHalfOpenIdentifiedCdc/CDC clauses
device-info re-identify gating
6 / 6
retries a brand-new (unseeded) connection that has not yet identified
retries a CACHE-SEEDED connection (stale deviceInfo, not yet fresh) — the inkdeck bug
STOPS once a live device_info has landed on this connection
stops after the request budget is exhausted
paces requests — no retry inside the retry interval
does not retry a disconnected connection
serialOpenFailureBackoffMs
3 / 3
ramps 10→20→40→80→160→300s and reaches the 5-min block at the 6th failure
never exceeds the 5-min block for any transient failCount
uses the flat 5-min block for EACCES (permanent) failures
▦
Display Rendering
PASS
Is image data for external displays rendered correctly?
19 passed
0 failed
1 files
69ms
pixoo-sprites.test.ts
19
/
19
69ms
getOctopusPaletteForSession
3 / 3
keeps the first additional session near the original terracotta tone
darkens later sessions while preserving channel ordering
clamps very large session indices to the darkest supported tone
creatureCellSize
10 / 10
is monotonic non-decreasing across a zoom sweep (cols=24, 32px)
is monotonic non-decreasing across a zoom sweep (cols=14, 32px)
is monotonic non-decreasing across a zoom sweep (cols=12, 32px)
is monotonic non-decreasing across a zoom sweep (cols=8, 32px)
is monotonic non-decreasing across a zoom sweep (cols=24, 64px)
is monotonic non-decreasing across a zoom sweep (cols=14, 64px)
is monotonic non-decreasing across a zoom sweep (cols=12, 64px)
is monotonic non-decreasing across a zoom sweep (cols=8, 64px)
is stable for sub-0.25 zoom jitter around a value (no flicker)
never returns less than 1
Codex cloud low-detail grids
2 / 2
keeps the MD cloud as one rounded body instead of separated dangling lobes
keeps the LOD cloud rounded for iDotMatrix-wide shots
quantizeCameraPixels
4 / 4
snaps the center onto the device-pixel grid
is idempotent
preserves zoom and width
defaults width to 64 and tolerates degenerate scale
▧
Hook Installation
PASS
Are Claude Code hook installation, removal, and migration safe?
44 passed
0 failed
1 files
314ms
install.test.ts
44
/
44
314ms
Hook Installer
30 / 30
buildHookEntry › creates matcher-group format with AGENTDECK_PORT env var
buildHookEntry › uses `*` matcher for tool events and empty matcher for lifecycle events
buildHookCommand (POSIX) › reads PORT from AGENTDECK_PORT env var first, then daemon.json, then 9120
buildHookCommand (POSIX) › keeps strict port-validation markers in every co-owned hook writer
buildHookCommand (POSIX) › rejects non-numeric and out-of-range ports before URL construction
buildHookCommand (POSIX) › accepts a validated daemon.json port
buildHookCommand (POSIX) › bounds the fire-and-forget POST so a wedged daemon cannot stall session exit
buildHookCommand (POSIX) › emits newline-separated shell so if/then/for/do keywords are not mis-terminated by `;`
buildHookCommandWin (Windows) › wraps a PowerShell one-liner that targets the event endpoint
buildHookCommandWin (Windows) › uses single-line PowerShell so cmd.exe can pass it as one -Command argument
buildHookCommandWin (Windows) › omits the macOS App Store sandbox-container fallback paths
buildHookCommandWin (Windows) › reads stdin as UTF-8 and posts UTF-8 bytes with charset (#46)
applyHooks › installs hooks to empty settings in matcher-group format
applyHooks › preserves non-AgentDeck hooks
applyHooks › replaces old flat-format hooks
applyHooks › replaces old matcher-format hooks
applyHooks › is idempotent — running twice produces same result
applyHooks › preserves existing non-hook settings
removeHooks › removes all AgentDeck hooks (new format)
removeHooks › removes old flat-format AgentDeck hooks
removeHooks › preserves non-AgentDeck hooks
removeHooks › handles empty settings gracefully
migrateHooks › migrates old hardcoded port to env var
migrateHooks › migrates flat format to matcher-group format
migrateHooks › skips already-migrated hooks (new format)
migrateHooks › skips non-AgentDeck hooks
migrateHooks › migrates multiple events at once
migrateHooks › migrates hardcoded port inside matcher-group
migrateHooksIfNeeded (file-based) › upgrades old :-9120 fallback hooks to daemon.json-reading format
migrateHooksIfNeeded (file-based) › self-heals installed hooks that predate strict port validation
steering hook channels (request-response)
2 / 2
PreToolUse and Stop echo the daemon response to stdout; others stay fire-and-forget
migration 5 rewrites legacy Stop hooks to the current platform hook set (request-response on POSIX)
install target (~/.claude/settings.json)
5 / 5
installs into the watched settings.json, never settings.local.json
strips AgentDeck hooks out of the legacy file while keeping the user keys
relocates pre-move installs from the legacy file into settings.json
leaves a settings.json without AgentDeck hooks untouched
uninstalls from both the current and the legacy file
Kiro v3 global hook installer
3 / 3
skips hosts where Kiro has never created its config root
writes the measured v1 standalone schema with prefixed lifecycle endpoints
is idempotent, preserves an occupied path, and only removes its own file
migration 7 — unbounded curl self-heal
3 / 3
flags a pre-fix hook set that every earlier migration passes by
rebuilds unbounded hooks on migrate and then leaves the file alone
does not flag the request-response hooks for lacking a short timeout
uninstall wiring
1 / 1
the uninstall entry point actually calls the Kiro remover
○
Other Tests
Uncategorized test files
2287 passed
0 failed
183 files
adb-transport.test.ts
4
/
4
3ms
isNetworkAdbTransport
4 / 4
flags adb-over-TCP serials (adb connect :)
flags wireless-debugging mDNS instance names
passes USB serials through
does not flag the local emulator
antigravity-transcript.test.ts
4
/
4
5ms
antigravity transcript parsing
4 / 4
extracts goal, model (from settings change), state and tool task
reports processing when the newest step is RUNNING; later model change wins
reports idle when only historical steps are RUNNING (newest step is DONE)
matches the agy CLI binary as an Antigravity process
apme-abandoned-run-reaper.test.ts
7
/
7
79ms
abandoned APME run reaper
7 / 7
finds runs with real work that the empty-shell reaper cannot see
measures staleness from last activity, not started_at
counts steps and sample events as activity, not just turns
closes turns, tasks and the run at the last activity, not now
is idempotent and drops out of the candidate list once reaped
leaves reaped runs to the normal eval queue
reports a run the collector still owns as live
apme-category-e2e.test.ts
3
/
3
53ms
APME category-aware turn evaluation (E2E)
3 / 3
conversation turn: prompt → classify → judge → turn category + composite persisted
research turn: rule-based classifier picks research, judge uses research rubric axes
daemon backfill pass: turns with response but no outcome get committed + null composite
apme-classifier.test.ts
18
/
18
86ms
classify()
12 / 12
planning — plan mode used
planning — short session, no file changes
research — web searches dominant
research — grep/glob dominant, no file changes
coding — edit+write dominant with file changes
debugging — tests + edits + bash
refactoring — many edits, no new files
review — mostly reads, minimal edits
ops — bash dominant, few edits
conversation — very short, no tools
multi_agent — multiple Agent delegations
unknown — no clear pattern
computeSignals()
3 / 3
counts tool calls and identifies dominant tool
detects plan mode from step payload
detects web searches
classifyRun()
2 / 2
returns signals and category for a coding run
returns unknown for empty runs
task_category schema migration
1 / 1
runs table has task_signals, task_category, task_category_source columns
apme-collector.test.ts
15
/
15
233ms
ApmeCollector
15 / 15
openRun → ingestHook → closeRun persists a run with steps
updateUsage and updateModel reflect in the run row
listRuns filters by agent and orders by started_at desc
default rubric v1 is seeded on init
insertEval + listEvalsForRun round-trip
excludes _empty runs from the unevaluated run queue
scorecard view aggregates runs per model
multi-turn cycle captures prompts and responses (wireAgentApme path)
setLastClosedTurnResponse fills missing response on closed turn
setTurnResponse tags efficiency_json.response_kind (text / tool_only / empty)
no-op gracefully when store is disabled
ignores an echoed duplicate turn_start (same prompt on a fresh empty turn)
treats a same-prompt re-send after a response or tool use as a new turn
an agent_error span records the failure reason in the trajectory without closing the task
drops an agent_error span with no open task instead of inventing one
apme-dashboard-html.test.ts
4
/
4
4ms
apmeDashboardHtml — data-quality affordances
4 / 4
explains an un-scored completed run instead of showing a bare header
surfaces manual reviews as a distinct, layer-filtered section
each aggregate tab has a graceful empty state (no NaN / blank grid)
active sessions are flagged as pending, not silently score-less
apme-http.test.ts
13
/
13
282ms
APME HTTP routes
13 / 13
returns 503 when APME is not initialized
GET /apme/runs lists recent runs with evals and overall score
GET /apme/runs filters by agent
GET /apme/run/:id returns run detail with steps and evals
GET /apme/run/:id returns 404 for unknown runs
GET /apme/run/:id includes per-task rollup with attached evals
GET /apme/tasks/:id returns task detail with evals + turns
closeTaskExternal with manual signal records outcome + boundary_signal=manual
GET /apme/scorecard returns model aggregates
GET /apme/rubric/current returns seeded rubric v1
POST /apme/vibe records feedback and rejects unknown runs
POST /apme/recommend returns candidate list
unknown /apme/* path returns 404
apme-judge-detect.test.ts
5
/
5
7ms
detectLocalJudgeProviders
5 / 5
drops embedding-only Ollama models
reports no provider when every Ollama model is an embedder
keeps models from servers that report no capabilities at all
still detects OpenAI-compatible servers via /v1/models
returns nothing when no local server answers
apme-judge-probe.test.ts
19
/
19
105ms
probeJudgeBackend — MLX
4 / 4
returns unavailable when /v1/models is unreachable
returns unavailable when /v1/models advertises no chat-capable model
returns unavailable when chat ping fails (model not loaded)
returns ready when catalog AND chat ping both succeed
probeJudgeBackend — OpenClaw
4 / 4
returns unavailable when /health is unreachable
returns unavailable when /health is up but /models is not (gateway not initialised)
returns unavailable when configured model is not in /models catalog
returns ready when /health + /models both up and model matches
probeJudgeBackend — Anthropic API (opt-in backend)
1 / 1
returns unavailable with setup guidance when no credential is present
callJudgeWithMeta — effective backend labelling across fallback
3 / 3
FM happy path labels eval as foundationModels
FM→MLX fallback (fallbackToMlx=true) labels eval as mlx, not foundationModels
FM failure without fallbackToMlx propagates the error (no silent label drift)
sanitizeForMlx — runtime fallback hygiene
4 / 4
strips FM endpoint and model when forcing to MLX
strips OpenClaw endpoint and model when forcing to MLX
returns the same cfg shape when already-clean MLX is passed
clears a custom MLX endpoint when source backend is MLX but model is overridden
probeJudgeBackend — Foundation Models
3 / 3
returns unavailable when Swift daemon FM endpoint cannot be resolved
returns unavailable when FM endpoint responds with error body
returns ready when FM endpoint responds with text
apme-manual-review.test.ts
2
/
2
36ms
manual_review eval roundtrip
2 / 2
persists a manual_review eval on a task and reads it back with the layer flag intact
manual_review evals do not contaminate the automatic-layer filters
apme-observed-turn-response.test.ts
4
/
4
61ms
observed turn response archiving
4 / 4
archives the reply on the turn and as an assistant_message event
tags a tool-only turn so the judge skips it instead of scoring silence
keeps each turn's reply on its own turn across a multi-turn task
lands a late reply on the last closed turn without overwriting one
apme-pareto.test.ts
4
/
4
30ms
computePareto
3 / 3
keeps non-dominated points and drops dominated ones
sorts the frontier cheapest → most expensive
ignores models below minSamples
ApmeRecommender on the Pareto frontier
1 / 1
recommends only frontier models, cheapest-first under a tight budget
apme-runner.test.ts
41
/
41
740ms
detectLanguage
4 / 4
recognizes TypeScript from package.json
recognizes Swift from .xcodeproj
recognizes Kotlin from build.gradle.kts
returns null for unknown paths
parseJudgeJson
5 / 5
extracts scores and reasoning from a clean JSON blob
rescales 0-10 axis to 0-1
tolerates prose wrapping and code fences
returns null when there is no JSON at all
returns null when overall is missing
effectiveJudgeModelTag
4 / 4
uses cfg.model for MLX when no pin is set
uses llm.mlx pin over cfg.model for MLX backend
ignores llm.mlx pin for non-MLX backends
foundationModels uses the Swift-parity judgeModelLabel
callJudge foundationModels routing
4 / 4
returns FM text on ok response
throws when FM reports unavailable and fallback is disabled
falls back to MLX when fallbackToMlx is true
caches auto-resolved FM endpoint failure and routes subsequent calls straight to MLX
shouldJudge gating
4 / 4
never runs when sampleRate is 0
skips clear passes when onlyWhenDisagreement is true
runs on failures when onlyWhenDisagreement is true
runs for clear passes when onlyWhenDisagreement is false
ApmeRunner duplicate guard
1 / 1
does not enqueue the same run while it is already queued or running
ApmeRunner.runOne
5 / 5
records deterministic failures and invokes judge with rubric prompt
skips layer 2 on a clean pass when onlyWhenDisagreement is true
does not notify onResult for no-op run evals
enqueueTurn skips judge for tool_only and empty turns
swallows judge errors without inserting partial rows
runDeterministic (end-to-end spawn)
3 / 3
runs sh-based commands against the project path and reports pass
captures exit code 1 as tests_pass=0
skips entirely when the worktree has no changes
onTaskEvaluated
3 / 3
fires after enqueueTask resolves and carries the composite score + outcome
derives outcome=fail for low composite scores
preserves a manually-set outcome — judge does not overwrite `abandoned`
buildJudgePrompt
2 / 2
includes rubric prompt + task + context fields
collects the diff with user diff drivers disabled (--no-ext-diff)
judgeBackendSupported
3 / 3
darwin supports every backend
darwin-only backends (foundationModels, mlx) are unsupported elsewhere
portable backends stay supported off-darwin
judge failure backoff
3 / 3
a failed judge blocks immediate re-enqueue and retries after the backoff expires
a successful judge clears the failure state and persists evals
parks the run after JUDGE_RETRY_MAX_ATTEMPTS failures until restart
apme-sample-timeline.test.ts
8
/
8
7ms
sampleEventToTimeline projection
4 / 4
projects user_message → chat_start
projects a text assistant_message → chat_response but skips tool_only
projects tool → tool_resolved with status mapping + input summary
skips model and state events (no standalone row)
OpenClaw session.tool / session.message → spans (previously dropped)
4 / 4
maps a running tool to a tool_call span carrying input
maps a completed tool to a tool_result span carrying output
maps a user message to turn_start and assistant to turn_response
drops empty/blank messages and nameless tools
apme-sample.test.ts
7
/
7
88ms
ApmeStore sample events
5 / 5
round-trips typed trajectory events into a SessionSample
dedups identical (task, dedupKey) at storage time
resolves a pending tool event in place (one row, not two)
recomputeSampleCost sums ModelEvents into the task header
local models price at $0; unknown models fall back to $0 but flag unpriced
ApmeCollector → SessionSample (Phase 1 dual-write)
2 / 2
builds a typed trajectory (user → tool pending→resolved → assistant) and cost
fires onSampleEvent once per inserted event (no dup double-emit)
apme-scorers.test.ts
6
/
6
6ms
TrajectoryQualityScorer
3 / 3
penalizes consecutive identical tool calls (churn)
penalizes tool errors
does not apply with fewer than 2 tools
ToolEfficiencyScorer
1 / 1
is the success rate of resolved tools
runSampleScorers
2 / 2
runs only applicable scorers and tags layer/scorer
returns nothing for a toolless conversation sample
apme-settings.test.ts
14
/
14
11ms
loadApmeConfig — defaults + merge behaviour
11 / 11
returns full defaults when settings.json does not exist
returns full defaults when settings.json has no apme section
returns defaults on malformed JSON (must not throw)
merges user-set fields on top of defaults
clamps sampleRate to [0, 1]
falls back to mlx when judge.backend is unknown string
honours judge.backend="api" (opt-in Anthropic API judge) without downgrading
also wipes endpoint/model when falling back from unknown backend → "mlx"
preserves user-set endpoint/model when keeping their chosen backend
respects explicit enabled=false (opt-out)
clamps deterministic.timeoutSec to [5, 1800]
loadApmeConfig — DEFAULT_APME_CONFIG sanity
3 / 3
default backend is Foundation Models with local MLX fallback (cost-sensitive policy)
default sampleRate is 1.0 (judge every closed run; cost is local)
default enabled is true (zero-config activation)
apme-stop-delivery.test.ts
21
/
21
266ms
turns.end_source
17 / 17
a real Stop closes the turn and tags it `stop`
a watchdog-injected Stop is tagged `synthetic_stop`, not `stop`
a turn still open when the next prompt arrives is tagged `next_prompt`
a duplicate/late Stop cannot re-attribute an already-closed turn
`ended_at` is stamped at the Stop, not at the following prompt
closing the run tags the open turn `session_end`, `/clear` tags it `clear`
a cancel is tagged `interrupted`, even though it arrives as a synthetic Stop
a client abort is tagged `aborted`, even though it arrives as a synthetic Stop
a cancel outranks an abort when both flags somehow arrive
a displaced turn whose transcript shows a cancel is `interrupted`, not `next_prompt`
a displaced turn whose transcript shows a client abort is `aborted`
a displaced turn the assistant never answered is `superseded`, not a lost Stop
a displaced turn that called tools is never `superseded`, whatever the transcript says
no evidence in the window still means `next_prompt` — absence is never a verdict
never reads a transcript for a non-Claude agent or a pathless hook
an unreadable transcript falls back to `next_prompt` rather than guessing
the response still lands on the turn when the Stop closes it first
ApmeStore.stopDelivery
4 / 4
buckets each closing signal and keeps open turns in the denominator
counts cancels in their own column, outside every loss bucket
reports pre-instrument rows separately instead of guessing a bucket
windows on started_at and can filter to one agent
apme-task-boundary.test.ts
15
/
15
327ms
ApmeCollector task boundaries
13 / 13
first UserPromptSubmit opens a task and attaches the turn
TodoWrite all-completed is a soft hint — does NOT close the task
TodoWrite all-completed does not split — later turns stay in one task
TodoWrite with partial completion does NOT close the task
next UserPromptSubmit after an explicit boundary opens a new task
splitRun closes the active task with boundary=clear
closeRun closes the active task with boundary=session_end
onTaskClosed fires on an explicit boundary with the task metadata
onTaskOpened is DEFERRED: fires on the second turn, not the first
onTaskOpened also promotes on a TodoWrite plan within the first turn
groups follow-up prompts under one header; single-turn stays headerless
onTaskClosed payload includes session, agent, project, and timing
empty task (no turns between two boundaries) is dropped
ApmeRunner task eval
2 / 2
enqueueTask invokes judge with turns and persists summary + scores
skips task eval when all turns are tool_only / empty
apme-task-milestone.test.ts
2
/
2
32ms
TodoWrite-all-completed → onTaskMilestone
1 / 1
fires once per (task, turn) with attribution; not on partial todos
opencodeIdleGapTaskBoundary
1 / 1
builds a task_boundary span with the idle_gap signal
apme-task-page-and-graph.test.ts
17
/
17
146ms
task page
6 / 6
carries the run context a bare task row lacks
counts turns whose reply was never archived
pages without losing the unfiltered total
filters by agent, project and open/closed state
hides bookkeeping shells the collector tagged _empty
offers facets from the whole store, not the current page
tool payload path extraction
4 / 4
reads a path from the tool input, wherever the shape puts it
returns null rather than inventing a path
keys a file on its path relative to the project root
falls back to the path tail without claiming two paths are one file
graph projection
7 / 7
projects the containment spine and the derived entity hubs
connects two tasks that share only a file — the cross-link no ancestor gives
collapses repeated calls into one weighted edge
draws the /clear continuation edge only when both runs are in the slice
prunes one-off hubs but never containment nodes
reports what it left out so a partial slice never reads as complete
attaches trajectory to turns when asked, and to the task when not
apme-telemetry-envelope.test.ts
33
/
33
464ms
claudeHookToSpans
7 / 7
emits turn_start with prompt for UserPromptSubmit
detects /clear and emits a task_boundary span (not turn_start)
drops Claude task notification payloads from UserPromptSubmit
emits tool_call for PreToolUse with gen_ai.tool.name
emits tool_result for PostToolUse
falls through to raw_step for unknown events (Stop, SessionEnd, …)
handles legacy { prompt: ... } shape on UserPromptSubmit
timelineEntryToSpans
12 / 12
translates chat_start to turn_start with detail as prompt
translates chat_response to turn_response
translates chat_end with response detail to turn_response with fallback flag
drops chat_response / chat_end with empty body
translates tool_request with first-token tool name
codex_user_prompt_submit → turn_start span
codex_user_prompt_submit with /clear → task_boundary span
codex_tool_start → tool_call span with tool name
codex_tool_end → tool_result span
codex_stop / codex_session_start / codex_turn_complete → raw_step
translates tool_exec the same as tool_request (legacy + Codex paths)
translates tool_resolved to a tool_result span
ApmeCollector.ingestSpan dispatch
13 / 13
turn_start span opens a turn and records the prompt
turn_response span persists the response on the active turn
tool_call span increments tool_calls on the active turn
chat_end fallback does not clobber a prior closed turn response
Codex timeline path opens turn and counts tools (timelineEntryToSpans)
task_boundary span with signal=clear splits the run
task_boundary span with signal=manual closes the active task
task_boundary span with signal=idle_gap closes the active task
task_boundary span with signal=todo_complete is a soft hint — does NOT close the task
task_boundary span with an unknown signal is dropped (no task close, no throw)
raw_step span inserts a steps row without lifecycle effects
session_meta span updates the model id on the run
timeline adapter feeds the same dispatch path correctly
eval-schema constants
1 / 1
exports a stable schema version string
ask-gate-decisions.test.ts
20
/
20
10ms
askGateDecision — who pays for the hold
4 / 4
holds only when there is no way to type into the session
never holds when the answer could be typed instead
never holds a session no device can see
never holds with the gate off or nobody connected
askPressVerdict — what may commit an answer
9 / 9
accepts a press that names the live question
refuses a press that does not name its question
refuses a press aimed at a question the prompt moved past
refuses to answer an overlay the held gate does not own
refuses anything that is not an option selection
refuses an index that is not in the live option list
matches on the option index, not its array position
refuses when no question is open at all
accepts an answer from a device that could only hold part of the question
askEchoMatches — a truncated echo still names its question
7 / 7
accepts an exact echo
accepts a long enough prefix
refuses a prefix too short to identify the question
refuses an echo that diverges from the live question
refuses an echo LONGER than the live question
accepts an echo cut inside a grapheme, as the Swift mirror does
refuses a superseded question that shares a long prefix
ask-gate.test.ts
6
/
6
10ms
AskUserQuestion ask-gate (device answer → agent)
6 / 6
delivers a single-question answer as the hook decision reason
walks a multi-question call one question at a time, then answers with all pairs
releases with an EMPTY body when nobody answers, so the terminal picker shows
does not hold when nothing could answer, leaving zero added latency
keeps a partially answered prompt out of the agent when the hold expires
clears the prompt on PostToolUse when the tool did run (injection path)
auth-token-adoption.test.ts
10
/
10
170ms
adoptPeerToken
7 / 7
serves the incumbent daemon token after adopting it
keeps accepting the superseded token so devices are not locked out mid-convergence
survives a restart — adoption is persisted, not in-memory
is a no-op when the incumbent serves the token we already hold
refuses a missing or malformed peer token rather than adopting a blank credential
bounds the accepted key ring instead of growing it forever
never records the same token twice in the ring
rotateToken
1 / 1
retires the accepted ring too, or rotation would not retire a leaked token
data-dir resolution
2 / 2
keeps the token beside the rest of the daemon state
inherits the legacy credential instead of un-pairing an existing fleet
awaiting-overlay.test.ts
39
/
39
40ms
awaiting-overlay
39 / 39
set then get returns the trimmed question
carries an optional requestId (actionable PreToolUse gate)
preserves a structured AskUserQuestion as a display-only option lifecycle
rejects malformed AskUserQuestion payloads instead of surfacing a dead prompt
does not let a generic permission Notification overwrite AskUserQuestion
returns undefined for an unknown session
survives a long away-wait, then expires past the generous backstop
re-setting refreshes the TTL (a follow-up prompt keeps the overlay alive)
clear removes the entry and reports whether one existed
caps question length at 120 chars
looksLikePermissionMessage › matches genuine permission prompts
looksLikePermissionMessage › rejects the idle ping, non-permission status text, and empty messages
isPermissionNotification › uses notification_type when present — only permission_prompt awaits
isPermissionNotification › falls back to the message regex only when notification_type is absent
shouldGatePreToolUse › gates in default / unknown modes (Claude may prompt)
shouldGatePreToolUse › never gates when Claude will not prompt or execute
shouldGatePreToolUse › acceptEdits auto-approves edits but still gates Bash
applyAwaitingOverlayToObserved › flips a matching observed session to awaiting_permission with the question
applyAwaitingOverlayToObserved › matches the uuid after stripping the observed:claude: / observed:codex: prefix
applyAwaitingOverlayToObserved › leaves sessions untouched when no overlay exists
applyAwaitingOverlayToObserved › propagates the requestId for actionable gates
applyAwaitingOverlayToObserved › renders AskUserQuestion as awaiting_option with real choices and no actionable requestId
transcript-recency supersession (ESC-dismissed prompt) › drops a display-only overlay once the transcript moves past it, and purges the entry
transcript-recency supersession (ESC-dismissed prompt) › keeps the overlay while the transcript stays frozen (genuine wait)
transcript-recency supersession (ESC-dismissed prompt) › holds within the jitter margin (a write at ~setAt is not a resolution)
transcript-recency supersession (ESC-dismissed prompt) › keeps the overlay when the session carries no transcript recency
transcript-recency supersession (ESC-dismissed prompt) › never drops a held device-approval gate on recency (its tool is blocked, resolve via onResolved)
transcript-recency supersession (ESC-dismissed prompt) › never drops AskUserQuestion on transcript mtime movement during a genuine wait
AskUserQuestion tool lifecycle › ignores unrelated or stale tool completions
AskUserQuestion tool lifecycle › clears only the matching PostToolUse/PostToolUseFailure id
multi-group AskUserQuestion progression › presents the first group and keeps the rest for later
multi-group AskUserQuestion progression › advances one group per answer and projects its question/options
multi-group AskUserQuestion progression › reports completion on the last group and keeps every Q→A pair
multi-group AskUserQuestion progression › surfaces group position to devices only when there is more than one
multi-group AskUserQuestion progression › skips malformed groups instead of presenting a dead prompt
multi-group AskUserQuestion progression › ignores advancement when no option prompt is pending
multi-group AskUserQuestion progression › clears every group on the matching PostToolUse
display-only notification flow (daemon restore contract) › permission_prompt notification → awaiting with NO requestId; next hook clears
display-only notification flow (daemon restore contract) › idle ping / auto-approved tool never sets the overlay (the 2026-06-27 false-attention bug)
ble-sync-squelch.test.ts
5
/
5
15ms
createSyncCycleSquelch
5 / 5
logs the first two occurrences of a cycle, then suppresses identical repeats
emits an hourly summary with the repeat count while suppressing
flushes and logs immediately when a different exit appears
treats 1x vs 2x captures of the same retry error as the same cycle
treats an exit after a long healthy run as a fresh incident
ble-sync-status.test.ts
14
/
14
9ms
parseStatusLine
2 / 2
parses an AGENTDECK_STATUS line
rejects a non-status line and malformed JSON
createBleLinkTracker
9 / 9
reports "sync not started" before anything is spawned
reports no device when nothing is configured
goes connected on a streaming status line — the regression this fixes
distinguishes host-display pause from a dead link
keeps hasFrame sticky across a disconnect and surfaces the error
clears a stale error once the client recovers
reports the backoff window after an exit, then falls back to connecting
surfaces a missing BLE runtime instead of an unexplained disconnect
a respawn clears the connected flag until the child reports again
mergeBleLinkSnapshots
3 / 3
is connected only when every panel is
reports no-device state when there are no trackers yet
takes the newest push time across panels
bridge-core-sessions.test.ts
2
/
2
78ms
BridgeCore sessions_list
2 / 2
broadcastSessionsList enriches sessions before broadcast
sendInitialState sends enriched sessions_list to the connecting client
calendar.test.ts
16
/
16
46ms
parseCalendarSettings
1 / 1
accepts a single url or a list, rejects everything else
ICS parsing
3 / 3
unfolds continuation lines
parses date, local, and UTC instants
parses VEVENTs with summary escaping and TZID params
occursOn (recurrence)
6 / 6
non-recurring: literal date only
DAILY with INTERVAL and UNTIL
WEEKLY honors BYDAY
DAILY COUNT exhausts
MONTHLY and YEARLY match the start day
EXDATE removes an instance
buildTodayEvents
4 / 4
keeps only what is left of today, all-day first, capped and sorted
an in-progress event survives the past filter via its end time
RECURRENCE-ID override replaces the master instance
trims titles to the UTF-8 byte budget
CalendarProvider
2 / 2
fetches, caches, and re-filters cached parses against the current clock
resolves undefined when unconfigured or on a cold failure
card-feed.test.ts
41
/
41
40ms
classifySessionCard
3 / 3
permission gate → live with the long-poll TTL
awaiting prompt without requestId → live with the awaiting backstop TTL
idle/processing sessions → info without expiry
buildCardFeed
4 / 4
derives one card per session with cardId session:
module cards (M7) follow the session projections and carry no session body
builds a module-only feed for an offline-first Pocket reader
pull cadence hint: idle roster → idle interval, active roster → active interval
applyOutboxDecisions
11 / 11
dismiss is always acknowledged, never dispatched
permission_decision applies only while the gate is still pending
permission_decision without requestId/decision is rejected
select_option routes session-scoped when the session is still awaiting
select_option expires when the prompt question changed
select_option expires when the session is no longer awaiting
unknown session → unknown_card
respond routes through session_command with the value
send_prompt queues via session_command for any alive session
malformed decisions are rejected without dispatch, order preserved
empty/absent decisions array → ok with no results
FeedPullTracker
8 / 8
normalizes v4-mapped and loopback client addresses
first pull carries no interval; the second measures it against the advertised cadence
a gap far off the advertised cadence is not counted as honoured
compares against the cadence advertised on the PREVIOUS pull, not the current one
learns the board from an outbox push and keeps it for later anonymous pulls
tracks clients independently and reports the median observed interval
bounds its history ring, newest first
formats the pull line with the drift verdict
conditional pull (deckSig)
7 / 7
stamps a deckSig and short-circuits to unchanged on a matching echo
a ticking elapsedSec does not defeat the signature
an idle roster stays signature-stable across a long clock advance (the idle-night case)
a real content change still breaks the match even when elapsedSec moves
expiresAt churn does not defeat the signature (live cards re-stamp every build)
content change breaks the match
glance is covered by the signature
glance builders
5 / 5
trimUtf8Bytes trims on rune boundaries by byte budget
buildGlanceUsage derives Claude and Codex rows with integer percents
buildGlanceUsage emits no row for a provider with no numbers
buildGlanceWrapup ranks attention first and folds overflow
buildGlance returns undefined when there is nothing to say
pull telemetry
3 / 3
parsePullTelemetry keeps in-range values and drops garbage
tracker records telemetry and unchanged counts per client
formats unchanged pulls and telemetry into the log line
card-modules.test.ts
17
/
17
12ms
module card ids
2 / 2
round-trips module::, including keys that contain colons
rejects session card ids and malformed ones
sealModuleCard — the ≤4-choice rule as a clamp
6 / 6
binds at most three choices, because slot 1 is the device Later
drops choices missing an id or a label rather than shipping a blank button
caps context lines and omits empty collections entirely
trims text to a UTF-8 byte budget on a code-point boundary
bounds stable choice ids for fixed firmware outbox buffers
stamps the card id and keeps the class/expiry the module asked for
buildModuleCards
1 / 1
a module that throws is skipped, the rest of the feed survives
THREAD module
4 / 4
produces nothing when there is no live session
leads with how many threads need the user
checkpoints an idle session with its last milestone
says what did not fit instead of implying the list is complete
card_choice routing
4 / 4
routes a choice to the authoring module with the card key
is terminal — never retryable — for unknown, read-only, or id-less cards
a throwing module reports error rather than taking the batch down
reaches the module through the outbox path, without touching session state
claude-interrupt-marker.test.ts
7
/
7
4ms
isClaudeInterruptRecord
6 / 6
matches both marker spellings Claude Code writes
matches on `interruptedMessageId` even if the marker text changes
does NOT match the marker quoted inside a tool_result
does NOT match an assistant that mentions the marker
does not match ordinary records or junk
tolerates an empty `interruptedMessageId` instead of trusting its presence
isClaudeInterruptMessage
1 / 1
reads string content as well as text blocks
claude-transcript-reader.test.ts
23
/
23
20ms
readLastTurn
7 / 7
extracts assistant text and user prompt from a text-only turn
flags tool-only turns (tool_use blocks, no text blocks)
concatenates text across multiple assistant records for one turn
picks the LAST user prompt when multiple turns are in the file
returns null for missing file
skips malformed lines and continues parsing
accepts legacy string content on both roles
readTurnEndProbe
8 / 8
reports end_turn with the record timestamp on a finished turn
skips trailing non-message records (mode markers)
reports tool_use mid-turn (prompt open / tools pending)
reports the user role while a tool_result is the tail (assistant pending)
returns null timestamp when the record has none, and null on unreadable file
skips a truncated final line (concurrent write)
flags the ESC interrupt marker as the turn end
does not read a quoted marker inside a tool_result as an interrupt
readOpenTurnEvidence
8 / 8
finds a marker buried under the retyped prompt that followed it
will not pay this turn for the previous turn's cancel
a stampless record does not hide markers behind it
returns null on a clean turn and on an unreadable file
distinguishes an unreadable transcript from one holding no evidence
reports the client abort that ends a turn with no Stop hook
will not pay this turn for the previous turn's abort
reports a turn that never ran — two prompts, no assistant record between
claude-turn-watchdog.test.ts
15
/
15
22ms
ClaudeTurnWatchdog
15 / 15
closes a turn whose Stop was dropped once the transcript shows end_turn
fires at most once per turn
ignores the previous turn's end_turn (timestamp before turn open)
closes a turn the user cancelled, tagged as an interrupt rather than a loss
closes a turn the client aborted (usage limit / auth / API error)
ignores the previous turn's abort
ignores the previous turn's interrupt marker
a plain user record is not an interrupt
never closes a genuine wait (stop_reason tool_use)
does not probe while the hook channel is active
disarms when the real Stop arrives
skips the transcript read when mtime has not advanced
disarms on SessionEnd without killing later recovery (/clear fires SessionEnd mid-session)
stop() (bridge shutdown) disarms permanently
waits without a transcript path and recovers once one is learned
cli.test.ts
36
/
36
50ms
agentdeck CLI parser
1 / 1
reports a misspelled top-level command as unknown and suggests the closest command
esp32-ota target resolution
5 / 5
maps short aliases to their canonical device_info.board string
leaves a canonical board string unchanged
passes an unknown target (e.g. a raw IP) through untouched for IP targeting
every alias resolves to a board that is itself a canonical entry (self-consistent SSOT)
drops the retired esp32_c6_147 board from the OTA target set
tokenizeArgString
4 / 4
splits bare flags on whitespace
collapses runs of whitespace and trims
keeps a quoted value with spaces as one token
returns [] for empty or undefined
applyGlobalEnvArgs
9 / 9
splices env tokens immediately after the session subcommand
inserts env tokens before user-typed flags (scalar options: retyping wins on last-write)
applies to monitor as well
is a no-op for non-session commands
is a no-op for a non-session command whose positional VALUE is a session word
is a no-op when the var is unset or empty
does not match a session word that appears as a flag value
skips the splice entirely when the user typed --no-env-args
strips an env-smuggled --no-env-args instead of letting it half-disable the layers
weaveAgentCommand
4 / 4
appends the per-agent var to the default command
appends onto an explicit -c command without replacing it
uses the agent-specific var and ignores others
returns the command unchanged when the var is unset or blank
resolveAgentCommand (per-agent half of the --no-env-args hatch)
2 / 2
weaves when env args are enabled
returns the command untouched when the hatch disabled env args
env defaults through real option parsing
3 / 3
an env-provided boolean flag sticks as the default
--no-env-args genuinely overrides an env boolean per-invocation
a retyped scalar option wins over the env default (last-write through parseArg)
daemon autostart posture
5 / 5
--enterprise is the admin spelling of the loopback posture
does not fold --local into --enterprise
a plain install carries no posture flags
the LaunchAgent argv carries the posture, one per flag
a plain LaunchAgent is byte-identical to the pre-posture one
claude action wiring (parseAsync end-to-end)
3 / 3
without the hatch: env flags parse in and the agent command is woven
--no-env-args disables BOTH layers for the invocation
a session bridge drives no device but ADB, even without --local
codex-auth.test.ts
7
/
7
5ms
resolveChatGptRenewalDate
7 / 7
rolls a stale monthly window forward to the next renewal boundary
rolls forward across many missed cycles in one step
rolls a stale annual window forward by a year
passes a future date through untouched
leaves a past date raw when there is no start to derive a period
leaves an untrustworthy (too-short) window raw
passes malformed and empty inputs through
codex-hook-silence.test.ts
5
/
5
15ms
CodexHookSilenceWarning
5 / 5
warns once when the PTY is active but no codex_* event ever arrived
stays quiet when any codex hook event arrives (channel proven)
stays quiet when the terminal was barely used (session may not have started)
warns late when activity only crosses the threshold after the deadline
stop() disarms (session shutdown)
codex-otel.test.ts
16
/
16
11ms
codex OTLP span parser
8 / 8
reads a full turn from dotted span names
normalizes underscore and slash span-name variants
rejects short numeric ids and prefers the durable alias on the same span
ignores a tool span own cwd but inherits the thread-scoped one
falls through to resource-level attributes
drops unknown spans and spans with no identity at all
summarizes span names for diagnostics
finds no durable identity in a live ChatGPT.app Codex batch
CodexOtelTracker
8 / 8
overlays state and tool onto the matching observed row instead of duplicating it
synthesizes a codex-app row only when the process scan found nothing
rejects a superseded turnEnd so a stale span cannot close the live turn
lets a follow-up turn revive a thread its own turnEnd retired
expires a finished thread, and a quiet one, instead of pinning a creature
stops overriding observer state once its own observation goes stale
never lets progress-only activity spans open or revive a thread
leaves non-codex rows untouched
codex-rate-limits-live.test.ts
21
/
21
412ms
parseLiveCodexRateLimits
4 / 4
maps the app-server shape onto the wire shape
accepts the rollout spelling of the window length
clamps out-of-range percentages
returns null when the result carries no usable limits
pickFresherCodexRateLimits
4 / 4
prefers the newer capture
keeps a passive reading that is newer than the cached live one
handles either side being absent
lets a stamped snapshot beat an unstamped one
shouldQueryCodexRateLimitsLive
4 / 4
queries when the passive snapshot has gone quiet
skips while Codex is mid-turn (the rollout is writing fresh readings)
honours the minimum interval between spawns
backs off hard after repeated misses (no Codex CLI installed)
codexSpawnPlan
5 / 5
runs a plain binary directly on posix
asks for a shell on Windows .cmd/.bat shims, which spawn refuses to run bare
quotes a shim path with spaces, since the shell re-parses the command line
leaves a real Windows executable alone — the shell is only for the shims
never asks for a shell on posix, even for a file that happens to end in .cmd
queryCodexRateLimitsLive
4 / 4
reads the rate limits out of the JSON-RPC stream
resolves null when the server never answers, without hanging
resolves null when the binary does not exist
resolves null when the server exits before replying
codex-rate-limits.test.ts
18
/
18
32ms
parseCodexRateLimitsFromText
9 / 9
parses primary/secondary windows and converts resets_at to ISO
returns the most recent snapshot when multiple lines are present
clamps used_percent into 0..100
tolerates a truncated leading line (tail window cut mid-line)
returns null when no rate_limits line exists
omits a window missing required fields
keeps a credit-based snapshot when windows are null
prefers a windowed snapshot over an older credits-only one
still returns null when neither windows, credits, nor limitId are present
pickCodexRateLimits (file selection across day-dirs)
6 / 6
returns null when the tree does not exist
picks the active prior-day rollout over a newer empty day-dir
falls through a newer empty file to an older one with data in the same day-dir
chooses the newest captured snapshot across concurrent active rollouts
invalidates the cache when a non-leading rollout writes a newer snapshot
reaches across a month boundary for a still-active prior-month session
capturedAt (snapshot freshness anchor)
3 / 3
stamps the snapshot line timestamp, not the file mtime
falls back to the file mtime when the line carries no timestamp
falls back to the file mtime when the line timestamp is unparseable
codex-rollout-response.test.ts
9
/
9
18ms
codex rollout response reader
9 / 9
locates a rollout by the session uuid embedded in the filename
prefers task_complete.last_agent_message (authoritative turn close)
falls back to the newest agent_message when no task_complete follows
returns empty for missing rollouts, malformed lines, and bad ids
failed turns › reports the error a task_complete carries instead of a reply
failed turns › reports a standalone error record
failed turns › unwraps the upstream JSON body Codex nests in the message
failed turns › never inherits the previous turn's reply
failed turns › still returns a successful reply unchanged
codex-turn-manager.test.ts
12
/
12
131ms
CodexTurnManager (hook-primary path)
8 / 8
happy path: UPS → tool_start → tool_end → stop emits one chat
a benign `message` on codex_stop is never rendered as an error
an explicit `error` on codex_stop renders the error close row
codex_stop does not reset subsequent turn_index numbering
codex_stop finalizes APME turn (endedAt set, tool_calls flushed)
next prompt opens a fresh chat_start
turn-complete notify closes a turn when codex_stop is absent
long-bash: hook tool_start + tool_end keep the same turn open
CodexTurnManager (stale-turn recovery on prompt-submit)
4 / 4
missed codex_stop: next prompt closes the stale turn and opens a fresh row
late prompt text fills a tool_start-opened turn without splitting it
duplicate prompt echo within the window is a no-op
text-less stale turn older than the fill window is closed, not filled
daemon-port.test.ts
15
/
15
14ms
daemon port — validation
2 / 2
accepts only integers inside the bindable range
rejects text that parseInt would silently truncate
daemon port — precedence
7 / 7
falls back to the documented default when nothing is set
orders flag › env › settings › default
reads a persisted port stored as a string (hand-edited settings.json)
ignores an unusable persisted port instead of refusing to start
ignores a malformed env override the same way
names every source
waits long enough to outlast the measured kernel port reservation
daemon port — persistence round trip
4 / 4
persists, reads back, and clears
preserves unrelated settings when writing
writes over a malformed settings.json rather than refusing
never records the port the daemon actually landed on
port window exhaustion message
2 / 2
separates ports this install owns from ports it cannot see into
does not tell a user to stop sessions they do not have
daemon-takeover.test.ts
16
/
16
41ms
negotiateIncumbentDaemon
7 / 7
takes neither the token nor the port from another user's daemon
refuses loudly when that port was asked for by name
still hands the port over between two daemons of the same user
proceeds when ownership cannot be proven, because absence is not evidence
falls back to /shutdown when the app is too old for /stand-down
a live Node daemon of ours means "already running", not a takeover
an empty port is not a negotiation
findDaemonPortAsync port scan
3 / 3
will not join another user's daemon, and says so instead of failing silently
joins a daemon of ours found by the same scan
joins a daemon whose ownership is unproven
localPeerOwnership
2 / 2
reads this process as ours and a dead pid as unknown
isForeignDaemon reports only a definite refusal
discovered-daemon selection
4 / 4
prefers this user's daemon over whichever answered first
demotes but never drops another user's daemon
an explicitly named host outranks even our own daemon
ranks a daemon that predates the TXT keys last, without excluding it
daemon-target.test.ts
27
/
27
19ms
daemon-target helpers
27 / 27
isLoopbackHost › treats loopback forms as local
isLoopbackHost › treats LAN/remote hosts as non-local
formatHostForUrl › brackets bare IPv6 literals
formatHostForUrl › leaves IPv4 and already-bracketed hosts untouched
parseHostHint › defaults the port to 9120 when none is given
parseHostHint › parses host:port
parseHostHint › does not mistake bare IPv6 for host:port
parseHostHint › parses bracketed IPv6 with and without a port
resolveDaemonTarget precedence + opt-in gate › prefers a local daemon and never consults remote paths
resolveDaemonTarget precedence + opt-in gate › refuses (null + warn) an incapable loopback daemon under --remote-daemon with no host hint
resolveDaemonTarget precedence + opt-in gate › falls through to a capable named host when the loopback daemon is incapable
resolveDaemonTarget precedence + opt-in gate › refuses an ssh -L tunnel to an incapable daemon named via --daemon-host 127.0.0.1
resolveDaemonTarget precedence + opt-in gate › prefers an explicit tokenHint over the daemon-advertised token
resolveDaemonTarget precedence + opt-in gate › uses tokenHint on the mDNS path too
resolveDaemonTarget precedence + opt-in gate › warns once (with provisioning hint) when a remote target has no token at all
resolveDaemonTarget precedence + opt-in gate › still returns an incapable local daemon without the switch (local default untouched)
resolveDaemonTarget precedence + opt-in gate › warnRemoteOnce logs a given key at most once (resolver re-runs on every backoff)
resolveDaemonTarget precedence + opt-in gate › ignores an explicit host without --remote-daemon: the switch gates BOTH remote paths
resolveDaemonTarget precedence + opt-in gate › resolves an explicit host with --remote-daemon, without touching mDNS
resolveDaemonTarget precedence + opt-in gate › returns null for an unreachable explicit host without falling through to mDNS
resolveDaemonTarget precedence + opt-in gate › refuses (null + warn) an explicit remote daemon lacking sameSocketControl
resolveDaemonTarget precedence + opt-in gate › runs mDNS discovery ONLY when the --remote-daemon switch is set
resolveDaemonTarget precedence + opt-in gate › never leaves the machine with no opt-in (no host, no switch)
deriveRemoteAttachOpts (shared CLI/index derivation) › is inert with no flags and no env
deriveRemoteAttachOpts (shared CLI/index derivation) › flags a host given without the switch as ignored
deriveRemoteAttachOpts (shared CLI/index derivation) › does not warn when the opt-in arrives via env alone
deriveRemoteAttachOpts (shared CLI/index derivation) › honors env host aliases in precedence order
daemon-ws-client.test.ts
6
/
6
577ms
DaemonWsClient register frame
3 / 3
always includes an explicit integer weight (unweighted ⇒ 0, never omitted)
carries the launch-time --weight value
normalizes garbage weights so the wire only ever sees in-range integers
DaemonWsClient target re-resolution (real socket, short backoff)
3 / 3
clears the cached target on a null resolution and only redials once a valid target resolves again
capability downgrade through the REAL resolver: incapable local daemon ⇒ no new socket (reviewer end-to-end case)
client guard: refuses to dial a non-null capability-less target under remote intent
default-settings-drift.test.ts
5
/
5
6ms
config/default-settings.json
5 / 5
parses as an object
carries only keys a loader actually reads
states the MLX defaults that shared/src/llm-settings.ts applies
states the APME defaults that loadApmeConfig() applies
does not resurrect the removed whisper / autoTune keys
device-discovery.test.ts
2
/
2
3ms
Pixoo discovery — config-reply classification
2 / 2
accepts a real GetAllConf reply
rejects a non-Pixoo HTTP/JSON response
device-photo.test.ts
10
/
10
15ms
DevicePhotoCollector
8 / 8
assembles frames into a JPEG file and reports the target session
ignores binary frames with no open capture
keeps captures separate per connection
returns null for a cancelled or empty capture
drops a capture whose byte count disagrees with the board claim
drops an oversized capture instead of writing a truncated JPEG
reaps captures whose photo_end never arrived
abandon drops the open capture for a dead socket
prunePhotoDir
2 / 2
keeps only the newest N photos
is silent on a missing directory
device-voice-reply.test.ts
33
/
33
14ms
speakableReply
5 / 5
replaces fenced code with a spoken marker instead of reciting it
strips markdown scaffolding and URLs that read as noise
returns empty when nothing speakable survives
cuts at a sentence boundary when the cap lands mid-sentence
hard-caps when no sentence boundary is near the cap
pcmFromWav
3 / 3
walks chunks rather than assuming the data starts at byte 44
rejects non-RIFF and truncated input
clamps a data chunk that claims more bytes than the file holds
pcmFrames
1 / 1
splits into fixed frames with a short tail
DeviceVoiceReplyRouter
10 / 10
only arms boards that advertise an amplifier
does not arm without a session id
matches only the session that was dictated to
expires an arming once the window closes, so a stale answer stays silent
keeps an arming across a transport blip, and refuses to stream while down
streams begin, paced PCM frames, then end
consumes the arming so one dictation yields one spoken reply
stops mid-stream when the board disconnects and sends no end frame
refuses a WAV it cannot parse rather than streaming garbage
disarm removes a target without streaming
session-id keying across the two forms
8 / 8
matches a bare-uuid completion against a prefixed arming
matches in the other direction too
normalizes the claude prefix
normalizes the codex prefix
normalizes the codex-app prefix
normalizes the opencode prefix
normalizes the antigravity prefix
still keeps different sessions apart
long-running turns
3 / 3
keeps an arming alive while the session is still working
still expires a session that went quiet
refresh accepts either id form and ignores an empty list
following the board across transports
3 / 3
streams to the board's current transport when the armed one is gone
gives up when the board is not reachable at all
prefers the armed transport while it is still open
device-voice.test.ts
18
/
18
81ms
dropSilentInterleave
5 / 5
drops a digitally silent odd slot (ips10 duplex shape)
drops a digitally silent even slot (flipped alignment)
passes true mono audio through untouched
still collapses when the stuffed slot carries rare glitch spikes
passes silence and tiny buffers through untouched
normalizePcm
5 / 5
boosts a quiet capture toward the target peak
caps the gain so near-silence does not become full-scale noise
leaves already-loud audio untouched
leaves pure silence untouched
clamps instead of wrapping at the int16 edge
buildWav
1 / 1
wraps PCM in a canonical 16-bit mono header
DeviceVoiceCollector
7 / 7
assembles frames into a WAV and reports the target session
ignores binary frames with no open utterance
keeps utterances separate per connection
returns null for a cancelled or empty utterance
caps a runaway utterance instead of buffering without bound
reports device-side and in-flight PCM loss before transcription
abandons and sweeps stale utterances
discovery-security.test.ts
5
/
5
14ms
LAN discovery credential boundary (issue #145)
5 / 5
never serializes a pairing credential into a UDP discovery beacon
keeps the beacon secret-free across periodic re-sends
does not materialize or log a token-bearing pairing URL in ../index.ts
does not materialize or log a token-bearing pairing URL in ../daemon-server.ts
keeps per-session project metadata off LAN discovery
display-dim.test.ts
3
/
3
5ms
display dim settings
3 / 3
defaults to enabled full-off when settings are missing
normalizes minimum-brightness settings
embeds the resolved dim instruction in display_state events
docs-wire-contract.test.ts
11
/
11
12ms
documented LAN boundary matches the gate
3 / 3
daemon.md quotes exactly the fields buildPublicHealth emits
the documented payload carries no credential, inventory or state
GET /health is the only route an unauthorized peer reaches
auth and wire specs stay visible in the published rail
5 / 5
docs/wire-compatibility.md is not in the collapsed group
docs/protocol.md is not in the collapsed group
docs/gateway-protocol.md is not in the collapsed group
docs/daemon.md is not in the collapsed group
groups them with the other surface specs
wire compatibility contract stays bound to its subjects
3 / 3
is cataloged in the design system
names helpers that still exist, so the id rules stay actionable
quotes the retain-on-absent merge shape the rules depend on
dot-matrix-glyphs.test.ts
18
/
18
73ms
canonical dot-matrix agent masks
2 / 2
ships every official agent mark at Pixoo/iDotMatrix and TC001 resolutions
preserves the official negative-space features instead of filled approximations
Timebox Mini Agent Beacon
10 / 10
renders octopus from a non-empty official 9×9 mask
renders jellyfish from a non-empty official 9×9 mask
renders opencode from a non-empty official 9×9 mask
renders crayfish from a non-empty official 9×9 mask
renders antigravity from a non-empty official 9×9 mask
renders kiro from a non-empty official 9×9 mask
keeps OpenCode hollow and OpenClaw teal-eyed at physical resolution
keeps identity fixed and moves only the perimeter rail while processing
renders a device-native standby tide when no agent exists
uses a continuous dim status frame instead of isolated floating pixels
iDotMatrix native 32×32 stage
1 / 1
keeps a vivid official mark and four source-keyed telemetry rails
Pixoo64 provider usage HUD
2 / 2
renders Claude and Codex as matching percentage-plus-reset bands
uses the empty Codex primary zone for the subscription date
Pixoo adaptive push policy
3 / 3
separates state latency from stable animation cadence
uses safe moving single frames for active Pixoo states
keeps the safe active cadence at 2.5 seconds
esp32-ota-stage-key.test.ts
6
/
6
13ms
pull-OTA staging
6 / 6
resolves an IP target to the board the advert is keyed on
keeps an unknown target verbatim rather than inventing a board
adverts an IP-targeted stage to the board that pulls the feed
reports pullSeen=false when no feed pull has ever been observed
reports pullSeen=true once that board has actually pulled the feed
resolves a target through the feed tracker when the board has slept out of the WS roster
esp32-transport-dedup.test.ts
7
/
7
3ms
isWifiTransportRedundant
7 / 7
dedups when the same board id is live on serial (both report matching IP)
dedups by board id alone when serial reports no IP (radio parked / pre-DHCP)
dedups when the WiFi side has no IP but the board id matches serial
does NOT dedup a WiFi-only board with no serial presence (86box: USB power-only)
does NOT dedup two distinct physical units of the same model on different IPs
never dedups an unresolved/unknown board identity
returns false when nothing is live on serial (WiFi is the only path)
esp32-wifi-ota-reconnect.test.ts
3
/
3
518ms
WiFi OTA reconnect-follow
3 / 3
completes a transfer across a mid-flight WS disconnect by resending on the reconnected socket
fails fast when the board drops mid-OTA and never reconnects
a clean transfer (no disconnect) completes with zero reconnect-resends
fallback-task-timeline.test.ts
4
/
4
5ms
FallbackTaskTimeline
4 / 4
opens a task on the first prompt and closes it on session end
segments on /clear and numbers the next task
ignores session end with no open task and tracks sessions independently
drops the per-session counter on session_end so counts cannot grow unbounded
fm-helper-arch.test.ts
10
/
10
5ms
machoMatchesArch
7 / 7
accepts a thin binary matching the requested arch
rejects a thin binary built for the other CPU
accepts a fat binary that carries a native slice
rejects a fat binary with no native slice
rejects non-Mach-O and truncated input
rejects a truncated fat header instead of over-reading
rejects unknown architectures outright
npm tarball determinism
3 / 3
never ships a baked helper binary
has no prepack hook baking machine-dependent artifacts
ships the helper source and build script for on-demand compilation
fm-helper-build.test.ts
4
/
4
3ms
fm-helper build invariants
4 / 4
declares a non-empty NSMicrophoneUsageDescription
declares a non-empty NSSpeechRecognitionUsageDescription
embeds the plist into the __TEXT segment at link time
ad-hoc signs with the identifier the plist declares
ships a binary carrying the usage strings
produces a signed binary macOS can prompt for
focus-relay-samesocket.test.ts
10
/
10
12ms
SessionFocusRelay — same-socket reverse control (primary)
10 / 10
focuses via the live push socket and sends session_focus_down
routes a command down the push socket as session_command_down
ignores commands that are not in the routed set
relays only RELAYED_EVENTS from the focused session up
sends session_unfocus_down on unfocus and clears focus
drops focus when the worker push socket closes
a stale socket close does not tear down focus held by a newer socket
migrateSender is a no-op for unfocused sessions and same-socket re-registers
stamps relayed prompt_options with the session identity
does nothing when neither a push socket nor a local session exists
gateway-parity-fixtures.test.ts
33
/
33
7ms
Gateway parity fixtures
33 / 33
fixture set is non-empty
'auth-pairing-required-error.json' carries a valid frame discriminator
'chat-delta.json' carries a valid frame discriminator
'chat-final-with-tools.json' carries a valid frame discriminator
'connect-challenge.json' carries a valid frame discriminator
'connect-hello-ok-device-token.json' carries a valid frame discriminator
'connect-ok.json' carries a valid frame discriminator
'exec-approval-requested.json' carries a valid frame discriminator
'health-event.json' carries a valid frame discriminator
'logs-tail-response.json' carries a valid frame discriminator
'models-list-response.json' carries a valid frame discriminator
'rpc-error.json' carries a valid frame discriminator
'session-message.json' carries a valid frame discriminator
'session-tool.json' carries a valid frame discriminator
'sessions-changed.json' carries a valid frame discriminator
'tick.json' carries a valid frame discriminator
'auth-pairing-required-error.json' conforms to its frame shape
'chat-delta.json' conforms to its frame shape
'chat-final-with-tools.json' conforms to its frame shape
'connect-challenge.json' conforms to its frame shape
'connect-hello-ok-device-token.json' conforms to its frame shape
'connect-ok.json' conforms to its frame shape
'exec-approval-requested.json' conforms to its frame shape
'health-event.json' conforms to its frame shape
'logs-tail-response.json' conforms to its frame shape
'models-list-response.json' conforms to its frame shape
'rpc-error.json' conforms to its frame shape
'session-message.json' conforms to its frame shape
'session-tool.json' conforms to its frame shape
'sessions-changed.json' conforms to its frame shape
'tick.json' conforms to its frame shape
chat-final fixture carries the final-state fields the adapter depends on
exec.approval.requested fixture exposes options for the user prompt
gateway-timeline-attribution.test.ts
3
/
3
2ms
enrichGatewayTimelineEntry
3 / 3
stamps agentType/projectName onto a bare OpenClaw adapter entry
preserves agentType/projectName the adapter already set
does not mutate the input entry
glance-frame.test.ts
11
/
11
370ms
wmoIconKey
1 / 1
maps WMO code groups to icon keys
renderGlanceFrameSvg
3 / 3
lays out the content and escapes markup
renders an honest empty state when there is nothing to show
ellipsizes long work lines before the hard column clip
packMono
3 / 3
packs MSB-first with 1 = white
pads row tails to byte boundaries
dithers mid-gray into a stable ordered pattern
renderGlanceFrame
4 / 4
produces the exact framebuffer size with a stable sig for stable input
rotates X3 into physical space with the device GfxRenderer Portrait mapping
sig ignores the clock stamp but tracks content — a clock tick must not void the conditional
landscape preset fills the X4 framebuffer
hook-codex-sessions.test.ts
10
/
10
10ms
HookCodexSessions
10 / 10
surfaces a Codex the process scan never found
yields to the observer instead of doubling the row
matches the observer row in either id form
tracks tool state through a turn
reaps a finished session, and one that died without a stop hook
lets a follow-up prompt revive a finished session
opens a row from a mid-turn hook when the session start predates the daemon
never lets a trailing tool callback resurrect a finished session
keeps the first cwd instead of relabelling on a later hook
ignores non-codex events and payloads with no session id
hook-server-usage-relay.test.ts
3
/
3
84ms
GET /usage — relay source contract
3 / 3
serves numbers together with a positive fetchedAt
withholds numbers that carry no timestamp (fetchedAt 0)
reports the same empty shape when no usage source is registered
http-auth-gate.test.ts
13
/
13
7ms
gateHttpRequest (issue #145 LAN default-deny)
3 / 3
lets an authorized request reach any route
serves only the public health payload to unauthorized peers
denies every other route to unauthorized peers
POST /pair — only a route while the operator holds a window open
5 / 5
is denied exactly like an unknown path when no window is open
opens for redemption only while a window is open
defaults to closed when the caller forgets to pass the window state
an open window does not unlock anything else, or any other method
still serves public health while a window is open
buildPublicHealth
1 / 1
never contains credentials, module inventory, or session state
isAuthorizedHttpRequest
4 / 4
trusts same-machine connections without a token
accepts a valid query token from a remote peer
accepts a valid Authorization: Bearer header
rejects remote peers with a wrong or missing token
idotmatrix-settings.test.ts
3
/
3
3ms
iDotMatrix extra name prefixes
3 / 3
keeps the configured prefixes
drops blanks and non-strings instead of passing them to the scanner
treats a missing or malformed key as no extra prefixes
kiro-diagnostics.test.ts
12
/
12
28ms
Kiro privacy-safe diagnostics
12 / 12
correlates an explicit resume id without serializing content, paths, ids, or commands
distinguishes cwd correlation, ACP records, and process-only IDE observation
reports missing prerequisites without treating them as a command failure
only exposes allowlisted transcript schema markers
summarizes measured v3 message envelopes and recognizes completed turns
takes the spoken reply, not the trailing redacted Reasoning block
separates a not-yet-prompted transcript from an unrecognized one
reports an unreadable transcript as unrecognized, not as not-yet-prompted
counts only non-blank transcript lines as evidence of content
discovers the measured v3 workspace/session directory layout
projects measured SQLite v2 rows and recognizes their schema marker
uses prompt-history mtime as a content-free pending-turn marker
kiro-timeline-feed.test.ts
6
/
6
5ms
KiroTimelineFeed
6 / 6
emits the last turn — and only that — the first time it sees a session
cuts the seeded turn at its prompt, never mid-turn
emits only what appeared after the first sighting
emits the first row of a session that was empty when first seen
forgets a session that is no longer observed
orders rows across sessions and survives a reader that throws
kiro-transcript-timeline.test.ts
7
/
7
13ms
kiroTimelineForSession
7 / 7
turns a real transcript into paired chat rows
reads the timestamp as seconds, not milliseconds
gives every reply record in one turn its own timestamp
keeps thinking blocks and tool results out of the rows
honours since and limit against the row timestamps
returns nothing rather than guessing when it cannot read
finds a session under a workspace-hash directory, not just cli/
linux-service.test.ts
38
/
38
204ms
buildUnitFile
8 / 8
produces a well-formed unit with the three standard sections
runs the daemon in the foreground so systemd supervises the real process
carries the network posture in ExecStart, quoted like every other unit word
mirrors LaunchAgent KeepAlive via Restart=on-failure
waits for the network to be online
defaults the working directory to ~/.agentdeck
derives BOTH WorkingDirectory and Environment from one dataDirOverride
reflects the AGENTDECK_DATA_DIR override in WorkingDirectory and Environment
unit-file escaping (golden bytes)
18 / 18
emits NO Environment= line when the override is not set
passes a plain path with a space through quoted and unescaped
escapes % as %% (specifier expansion) in Environment and WorkingDirectory
C-escapes backslash and double quote inside the quoted Environment value
keeps $ SINGLE in Environment= (systemd does not expand $ there)
doubles $ and % in ExecStart words (expanded there even inside quotes)
escapeUnitEnvAssignment / escapeExecWord expose the per-directive rules
rejects control characters in the override (newline)
rejects control characters in the override (carriage return)
rejects control characters in the override (NUL)
rejects non-absolute overrides (relative path)
rejects non-absolute overrides (bare tilde)
rejects non-absolute overrides (tilde path)
rejects non-absolute overrides (ignore-missing prefix)
rejects edge whitespace the unit parser would trim (leading space)
rejects edge whitespace the unit parser would trim (trailing space)
rejects edge whitespace the unit parser would trim (trailing tab)
validateDataDirOverride accepts a hostile-but-legal absolute path
Environment= round-trip (systemd parse semantics)
6 / 6
round-trips the raw value (space)
round-trips the raw value (percent specifier)
round-trips the raw value (double quote)
round-trips the raw value (backslash)
round-trips the raw value (dollar)
round-trips the raw value (everything at once)
getDataDir
2 / 2
defaults to ~/.agentdeck
honors AGENTDECK_DATA_DIR
installUnit (fresh data dir)
2 / 2
creates a not-yet-existing data directory before touching systemd
rejects an invalid override BEFORE creating any directory
getUnitPath / getUnitDir
2 / 2
defaults to ~/.config/systemd/user
honors $XDG_CONFIG_HOME
mdns-discover.test.ts
5
/
5
61ms
discoverDaemons
5 / 5
returns a reachable daemon, preferring the /health pairingToken over TXT
filters out daemons that do not advertise sameSocketControl (e.g. Swift App Store)
drops link-local and loopback addresses
excludes candidates whose /health is not a daemon
falls back to the TXT ip when no usable addresses are present
mdns-hostname.test.ts
4
/
4
16ms
mDNS service hostname
4 / 4
never publishes A/AAAA records under the macOS system hostname
identifies which daemon it is without naming the account
never puts a pairing token in the multicast TXT record (issue #145)
keeps the service hostname stable across recovery re-publishes
network-posture.test.ts
18
/
18
13ms
module registry SSOT
3 / 3
MODULE_NAMES matches the modules createDefaultModules actually registers
allModulesOff() switches off every registered module
covers the two modules the hand-written --local records used to omit
resolveDaemonPosture
4 / 4
defaults to the open posture
reads the env switch
treats --loopback and the env var as the same switch
keeps --local and --loopback independent
daemonModuleConfigs
8 / 8
leaves everything enabled in the default posture
loopback silences every module that touches the network
loopback keeps USB serial — a board on a cable is not a network peer
loopback keeps ADB reverse — the tunnel rides USB into the host loopback
loopback still honours the AGENTDECK_DAEMON_NO_SERIAL diagnostic gate
--local turns off every module, serial included
--local wins over loopback when both are set
a module added later defaults to off in both restricted postures
describeDaemonPosture
3 / 3
names what is off, not just the bind address
folds the USB channels into the off-list when --local is also set
points at the switch when the daemon is wide open
observed-hook-classify.test.ts
8
/
8
4ms
classifyObservedHookEvent
8 / 8
passes Claude-mapped events through with claude-code attribution
maps codex_* lifecycle names to their boundary with codex-cli attribution
maps codex notify turn_complete to stop (single completion row per turn)
keeps Codex subagent hooks attributed to Codex
maps opencode_* plugin events to their boundary with opencode attribution
accepts antigravity_* for forward-compatibility (no installer yet)
maps Kiro 2.x and 3.x hook vocabulary without requiring managed launch
leaves unknown or unprefixed names untouched (claude fallback)
observed-inject.test.ts
18
/
18
9ms
parseTmuxPanes
2 / 2
maps pane ttys (dev-prefix stripped) to pane ids
ignores malformed lines
buildItermSelectScript
3 / 3
targets the session by tty and types Down x index then Enter
index 0 is a bare Enter
can emit the arrows without the Enter that would act on them
parseProcessTable with tty column
1 / 1
parses tty and treats ?? as none
buildTerminalAppSelectTabScript
2 / 2
selects the tab by tty without activating the app
returns notfound when no tab matches
buildPostKeysScript
3 / 3
matches by bundle id first, then localized name
posts to the pid (never activates) and ends with Return
carries a second Return only when a grouped prompt needs submitting
buildAppButtonPressScript
2 / 2
matches the button by label with prefix tolerance and clicks it
escapes quotes in labels
buildAppKeysScript
1 / 1
raises the app, keys, and restores the previous frontmost app
escapeAppleScript
1 / 1
escapes backslashes and quotes
app-host resolution
3 / 3
extracts the app name from a bundle path
walks the ancestry to find the hosting app
returns undefined for a plain terminal session
observed-steering.test.ts
44
/
44
51ms
shouldHoldPreToolUse — precision guards (never hold auto-approved calls)
17 / 17
holds a prompt-prone Bash call with no matching rules (the genuine-wait case)
never holds when disabled
never holds with zero connected clients (nobody can answer)
never holds never-prompt tools (Read/Glob/Grep/TodoWrite)
never holds MCP tools (per-server trust state is invisible)
never holds unknown tools (not in the prompt-prone set)
never holds in auto-approving permission modes
acceptEdits: never holds edit-family tools, still holds Bash
never holds when a user-level allow rule prefix-matches the command
never holds when a PROJECT-LOCAL allow rule matches (session "always allow" persists here)
never holds on a bare tool-name allow rule
never holds a non-Bash tool when any allow rule exists for it (loose spec match)
never holds when a deny rule may match (Claude auto-denies without prompting)
never holds when ANY settings file is unparseable (rule picture unknown)
compound command: allow rule on the first segment suppresses the hold
holds on a strict ask-rule match (Claude will definitely prompt)
allows only one held gate per session (parallel tool calls pass through)
auto-approval learner (session "always allow" is invisible — learn it)
4 / 4
suppresses a signature after undecided release + tool_end with no permission prompt
does NOT suppress when a permission prompt was shown in between (genuine wait)
does NOT learn from device-decided releases
signature granularity: Bash uses the first two command tokens
soft STOP
3 / 3
consumeStop is one-shot
clearOnUserPrompt clears stop + directives (user took over)
clearStop drops the flag without consuming semantics
turn-end directive queue
7 / 7
caps the queue and pops exactly one per takeDirective
rejects empty text
a pending STOP outranks directives: queue is discarded, nothing delivered
empty queue always returns undefined (no stop_hook_active loop)
a synthetic Stop leaves the queue intact — nothing is listening on that response
takeDirectiveForStop tolerates a missing payload
steeringSnapshot reflects both flags for device badges
evaluatePermissionRules verdicts
4 / 4
none when no files exist
exact vs prefix Bash specs
deny beats allow
ask rules never match loosely on unparseable specs (no over-holding)
beginAskGate (AskUserQuestion always prompts — no precision guards)
6 / 6
holds without consulting permission rules or the prompt-prone set
holds even where the rule surface is unreadable (the sandbox case)
never holds when disabled or with nobody to answer
keeps the one-gate-per-session invariant in both directions
releases the session so the next question can hold again
an unanswered ask teaches the auto-approval learner nothing
buildAskAnswerReason (the device answer reaching the agent)
2 / 2
states every Q→A pair and forbids re-asking
drops groups that were never answered
buildGateQuestion (device-native semantics, no TUI mirroring)
1 / 1
previews the Bash command / file path / url
observed-turn-watchdogs.test.ts
11
/
11
16ms
ObservedTurnWatchdogs
11 / 11
recovers a dropped Stop and reports the session it belongs to
keeps sessions independent — one session's Stop must not disarm another
one session's tool traffic does not keep another's watchdog quiet
a real Stop pre-empts recovery for that session
the synthetic Stop it triggers disarms it, so it fires once per turn
never closes a genuine wait (transcript still shows tool_use)
only admits a session that has submitted a prompt
sweep() drops cold sessions so their poll timers cannot outlive them
sweep() leaves a session that is still posting hooks alone
caps tracked sessions, evicting the coldest first
stop() (daemon teardown) kills every session timer
openclaw-approval-flow.test.ts
15
/
15
19ms
the user can see what they are approving
2 / 2
puts the real command on the timeline row and the prompt
exposes the prompt for the session row
the user can actually resolve it
6 / 6
sends a decision the Gateway accepts — never the bare "allow"
maps each option index to its own decision
resolves a shortcut `respond` press too
drops a press whose question echo names a different approval
accepts a truncated echo — small surfaces cut the question
keeps the prompt on screen when the resolve fails
the prompt goes away when the approval does
7 / 7
labels a real allow as approved (not denied)
a deny ends the turn rather than resuming it
closes the approval when the run is cancelled under it
closes the approval when the turn errors out
adopts an approval that was already waiting when the adapter connected
adopts the OLDEST waiting approval — that is the one blocking the agent
a resolution for a different approval does not clear the pending one
openclaw-chat-error.test.ts
4
/
4
10ms
OpenClaw chat error → timeline row
3 / 3
keeps the provider sentence as the row label and puts the turn context in detail
flags an errored gateway-initiated turn automated, like every other close row
describes a message-less error frame instead of writing the word "unknown"
OpenClaw chat error → APME lifecycle
1 / 1
records the failure and re-arms the idle-gap timer so the task can still close
openclaw-hook.test.ts
12
/
12
9ms
openclaw-hook → telemetry spans
12 / 12
OPENCLAW_IDLE_GAP_MS is conservative (60–180 s) so multi-turn collab stays together
chat.send produces exactly one turn_start span carrying the prompt text
chat.final with a response + tools yields turn_response + per-tool tool_result spans
chat.delta emits no spans — deltas are streaming chunks, not eval signals
chat.aborted emits a manual task_boundary so the user gesture closes the task immediately
chat.error emits an agent_error span — and NOT a task_boundary, the agent may retry
idle-gap task_boundary span carries boundary_signal=idle_gap
agent_error carries the turn context the Gateway frame omits
agent_error falls back to a describable label when the frame carries no message
omits detail entirely when it would only repeat the label
label is byte-equal to the projected row raw, so the two never render twice
all emitted spans propagate traceId for run correlation
openclaw-session.test.ts
7
/
7
6ms
injectOpenClawSession
7 / 7
does NOT inject when the Gateway is reachable but not authenticated (regression: phantom trace)
injects a minimal session when authenticated (CLI bridge shape)
carries daemon-hub extras (state/projectName/modelName/controlMode) when provided
carries the pending approval so decks render real, pressable options
leaves the prompt fields off when nothing is pending
is idempotent — does not duplicate an already-present openclaw session
returns the original array reference when not injecting (no needless copy)
openclaw-timeline-completion.test.ts
3
/
3
74ms
OpenClaw chat final → single completion row
3 / 3
emits chat_response only (no chat_end) when the turn has response text
marks gateway-initiated (cron) turns automated and skips LLM enrichment
emits chat_end (not chat_response) for a response-less turn
opencode-adapter-state.test.ts
11
/
11
26ms
OpenCodeAdapter run-state arming
6 / 6
arms processing on the first message.part.updated and latches once per turn
emits idle on session.idle and re-arms on the next turn
auto-tracks the session and arms when no active session was resolved at connect
arms when an assistant message is still generating (no time.completed)
does not arm on a completed assistant message alone
drops parts for a different active session
OpenCodeAdapter timeline turn shape
5 / 5
upserts the user prompt into chat_start when the user message carries text
routes user text parts to the prompt, not the response
emits chat_response XOR chat_end — response turns produce no chat_end
closes response-less (tool-only) turns with chat_end
resets prompt/response state between turns
opencode-client.test.ts
13
/
13
14ms
OpenCodeClient
12 / 12
health › should call /global/health
listSessions › should call /session with directory and limit
createSession › should POST to /session
sendMessage › should POST to /session/{id}/message with parts
sendMessage › should include model option when provided
abortSession › should POST to /session/{id}/abort
respondPermission › should POST allow response
respondPermission › should POST deny response
HTTP error handling › should throw on non-ok response
disconnect › should prevent reconnection after disconnect
url accessor › should return server URL
url accessor › should strip trailing slash
OpenCodeAdapter event mapping
1 / 1
should export OpenCodeSSEEvent type
opencode-hook.test.ts
9
/
9
8ms
opencode-hook → telemetry spans
9 / 9
tool part with status=running produces a tool_call span
tool part with status=completed produces a tool_result span
todowrite completion with all todos completed emits a task_boundary span (todo_complete)
todowrite completion with a pending todo does NOT emit a task_boundary
todowrite reads todos out of state.output JSON string when input is absent
non-tool part returns an empty span list
message.updated for user role with prompt emits a turn_start
message.updated for assistant role with response emits session_meta + turn_response
assistant message with modelID but no response still emits session_meta (early model attribution)
opencode-steering.test.ts
6
/
6
2.0s
opencode steering queue
6 / 6
poll drains queued commands immediately
a parked long-poll wakes the moment a command arrives
long-poll resolves empty after the wait window
permission_respond commands pass through with id + decision intact
caps the queue
steerability reflects a live poller
pairing-window.test.ts
41
/
41
37ms
window lifecycle
7 / 7
is closed until the operator opens one
mints a code of the documented shape
never repeats a code across many opens (i.e. it is actually random)
replaces a window rather than leaving two valid secrets running
closes on the operator asking
clamps a window length nobody should be able to ask for
clamps the device count to at least one
redemption
8 / 8
hands over the token for the right code and closes a one-device window
reads the token live, so a handover reaches a device pairing right now
never mints a token for a refused redemption
refuses everything once the window is closed
pairs several devices from one window, then closes
closes the window after the attempt budget, across peers
does not spend the budget on malformed submissions
accepts the code with the separators the display added
cable-free ESP32 re-arm
14 / 14
is off unless the operator named the board
adopts only the addresses the operator named
is closed when no window is open at all
expires with the window, enforced on read
re-arms a board once, not on every reconnect
reports the re-armed board to the operator
sanitizes a board name from an unauthenticated peer
names an unflashed board generically rather than blank
rejects malformed addresses instead of storing them
bounds how many boards one window may re-arm
shows what is still pending, so a silent board is visible
stays open for the code after every board is re-armed
stays open for a pending board after the code is redeemed
closes once both halves are spent
expiry is enforced on read, not by the timer
3 / 3
refuses a window whose deadline passed even if no timer ran
refuses the right code after expiry
is still open one millisecond before the deadline
operator status
9 / 9
never contains the code — the opener already has it, nothing else needs it
reports who paired and who guessed wrong
sanitizes the device label, which is untrusted text from an unpaired peer
falls back to a placeholder rather than an empty or non-string label
still reports the pairing after the window closed on it
does not report a previous run's pairings to the next window
forgets the receipt rather than keeping it for the life of the daemon
the receipt carries no code and no token
counts down in whole seconds and never shows 0 while open
passive-observer.test.ts
32
/
32
33ms
passive-observer parsers
32 / 32
expires a silent observed processing state without hiding fresh work
parses ps output without depending on fixed command columns
parses Win32_Process JSON, converting bytes to KB and dropping null command lines
parses the bare object ConvertTo-Json emits for a single row
returns no rows for non-JSON scan output
parses scan output that arrives with a UTF-8 BOM
summarizes Claude transcripts and redacts tool secrets
reads idle after an ESC/interrupt marker aborts a pending tool_use
summarizes Codex rollout metadata, context, and pending tool calls
reads idle after task_complete even when sampling dropped tool outputs
reads idle after turn_aborted with dangling tool calls
clears stale pending calls from a prior turn when a new user message arrives
stays processing through mid-turn thinking gaps and agent messages
maps lsof field output to Codex rollout files by pid
keeps every rollout a desktop app-server pid holds open, deduped
admits Codex Desktop app-server but rejects helper lookalikes
matches Win32 command lines — quoted paths, backslashes, .exe suffix
finds an npm-installed Claude Code, which never puts "claude" in argv[0]
drops Electron child processes that reuse the app binary name
matches Kiro CLI and IDE owners while rejecting Electron helpers
collapses the Kiro launcher and kiro-cli-chat child to one chat process
collapses the real four-deep chat tree, acp child included, to one process
reads Kiro 2.x metadata and transcript turn state
reads a Kiro tool subject out of the nested operations shape
reads documented Kiro ACP session notifications for the v3 fallback
correlates a naturally launched Kiro process with its newest cwd session
marks internal subagent rollouts from session_meta source
reuses unchanged rollout summaries and invalidates changes/removals
surfaces top-level Desktop rollouts and excludes internal subagents
dedupes a hook-observed Desktop conversation without hiding sibling rollouts
counts Codex cached input once when the producer folds it into input_tokens
recognizes standalone Antigravity processes for CLI daemon passive discovery
peripheral-mapping.test.ts
10
/
10
6ms
parsePeripheralMappings
2 / 2
keeps valid entries and drops malformed ones
returns empty when the block is absent or not an array
resolvePeripheralAction
5 / 5
matches a tag case-insensitively and honors the project filter
without a project filter targets the session that needs a human
matches IR codes too
returns undefined for an unmapped tag or an empty roster
does not cross kinds (an nfc uid never matches an ir event)
commandForAction
3 / 3
approve prefers a held gate and falls back to the live prompt
deny mirrors approve
maps focus / review / stop to existing daemon commands
permission-resolver.test.ts
12
/
12
13ms
permission-resolver
12 / 12
register → resolve(allow) ends the held response with allow
resolve(deny) ends with deny
times out to an EMPTY pass-through body after timeoutMs (never an explicit "ask")
resolving an unknown id returns null and is a no-op
double resolve: second returns null (already resolved)
abandonPending drops the entry without writing a decision
sweepStalePending releases entries older than maxAge as pass-through
re-registering a duplicate requestId releases the stale entry first
resolving over a dead socket does not throw and still clears the entry
fires onResolved exactly once on a device decision
fires onResolved with "pass" on timeout (arms the auto-approval learner)
drainAllPending releases every held response as pass-through
pixoo-autodiscover-default.test.ts
5
/
5
7ms
pixooAutoDiscoverFrom
3 / 3
is OFF when the setting is absent
is OFF for anything that is not an explicit true
is ON only when explicitly opted in
discoverPixoo cloud opt-out
2 / 2
queries the Divoom cloud by default
never touches the cloud when cloud is false
pixoo-creature-sync.test.ts
19
/
19
18ms
pixoo creature sync — Codex folding
5 / 5
draws one cloud per project, not one per codex thread
keeps separate projects as separate clouds
surfaces the busiest state in a folded group
does NOT fold Claude Code — several octopuses on one project is deliberate
ignores dead sessions
pixoo creature sync — per-type bands
3 / 3
clusters each agent type into its own X band
stratifies types vertically so a mixed tank stays readable
shrinks creatures as a band fills up
pixoo creature sync — the `_primary` fallback
4 / 4
synthesizes a creature before the first sessions list arrives
draws an empty tank once an empty list is known
does not synthesize a creature for a non-creature agent
does not stamp the stateEvent onto a real session
pixoo creature sync — unknown agent types
3 / 3
draws no creature for an agent type it has never heard of
does not let an unknown agent become a Claude octopus alongside real ones
gives every known agent type its own creature
pixoo creature sync — Usage HUD safe area floor
4 / 4
keeps normal ground depth (0.80) when no usage HUD is active
clamps idle creatures to 0.72 when a single-provider HUD is active
clamps idle creatures to 0.65 when a dual-provider HUD is active
does not alter upper/mid-water processing creatures when HUD is active
pixoo-usage-rails.test.ts
7
/
7
30ms
iDotMatrix 32×32 telemetry rails
5 / 5
spends no row on a provider that reports nothing
reads a windowless free-tier Codex block as no Codex rows
still fills all four rows when both providers report both windows
gives the panel back entirely when no provider reports usage
keeps a stale (ended) Codex window off the rails, not on a blank row
Pixoo64 usage HUD bands
2 / 2
drops the Codex band for a windowless free-tier block, and the lone Claude band slides down
draws no HUD at all when neither provider reports usage
pocket-autonomy.test.ts
9
/
9
13ms
AutonomousPocketEngine
8 / 8
cold-start explores a preference quest and the strongest available signal
learns from choices and stops repeating the cold-start survey
treats a retried outbox answer as idempotent
treats Later as a neutral read-state and does not return the same card
lets a read-only pulse be marked Done through the reserved Later action
persists only aggregate learning state and opaque fingerprints
counts delivered cards with no response as weak negative evidence after a day
routes only stable Pocket choices
parsePocketAutonomyConfig
1 / 1
defaults on absent settings and clamps explicit bounds
project-name.test.ts
18
/
18
108ms
resolveProjectName
10 / 10
returns git toplevel basename from a nested subdir
falls back to nearest package.json name when no git
skips package.json with empty name and keeps walking
falls back to cwd basename when no git and no package.json
returns 'unknown' for a basename-less root
AGENTDECK_PROJECT_NAME env var wins over everything
envOverride option takes precedence over env var
preserves scoped package name verbatim
git subprocess error (non-repo) does not throw
malformed package.json is ignored (walks to parent)
gitToplevelBasename
2 / 2
returns null outside a git worktree
returns repo basename from nested subdir
resolveProjectNameFromCwdCached (passive-observer resolver)
4 / 4
matches the PTY resolver for a git repo subdir (launch-path parity)
detects a .git FILE (worktree/submodule layout)
falls back to package.json name, then basename
memoizes per cwd (stale after dir changes are acceptable)
nearestPackageJsonName
2 / 2
returns null when no ancestor has package.json
returns nearest ancestor name
pty-manager-stdin.test.ts
2
/
2
6ms
PtyManager.attachTerminal
2 / 2
preserves multi-byte characters split across stdin chunk boundaries
forwards single-byte input unchanged
python-ble-runtime.test.ts
5
/
5
19ms
optional Python BLE runtime
4 / 4
resolves packaged scripts from the bridge package and keeps the venv in the data directory
reports missing npm assets instead of silently disabling BLE
creates and marks a persistent environment on first explicit setup
accepts a compatible legacy checkout venv without modifying it
bridge npm BLE assets
1 / 1
publishes every Python client and the dependency manifest
remote-reverse-e2e.test.ts
5
/
5
763ms
same-socket reverse control (worker ↔ daemon, real sockets)
5 / 5
registers over a single outbound socket with explicit remoteAttach intent
ignores down-frames addressed to a foreign session
emits the focus snapshot up when the daemon focuses (session_focus_down)
applies a command sent down the same socket (session_command_down)
forwards live events up only while focused
remote-sessions.test.ts
7
/
7
8ms
remote-sessions registry
7 / 7
registers a remote session with display host/port
keeps startedAt stable but refreshes target on re-register
re-register swaps the sender; the old socket cannot remove or overwrite
updates state only for known sessions
renders enriched rows marked alive/managed
normalizes remote weight and preserves it for legacy re-registration
removes a session
review-runner.test.ts
15
/
15
9ms
renderReviewHtml
3 / 3
is self-contained (no external asset URLs) and shows risk + findings
escapes HTML in judge-provided text (no injection)
empty findings renders a calm "nothing worth flagging" note, not an empty grid
judgeTierNote — honest capability labelling
3 / 3
flags on-device models as basic screening
flags local MLX depth as model-size dependent
does not append a caveat for frontier backends
renderJudgeGuidanceHtml — no-judge setup flow
3 / 3
ranks API → OpenRouter → OpenClaw → local → Apple, and states the local model minimum
reassures that not using REVIEW is a supported choice (no background nagging)
surfaces the current backend + reason so the user knows what failed
openAIChatUrl — endpoint normalization (Ollama / OpenRouter / LM Studio)
4 / 4
bare host → /v1/chat/completions
base with /v1 → appends chat/completions
already-full URL is preserved
trailing slash tolerated
renderJudgeGuidanceHtml — detected local providers
2 / 2
surfaces detected servers with a ready-to-paste openai config
omits the detected block when nothing is running
session-aggregator.test.ts
8
/
8
28ms
session-aggregator
8 / 8
uses ownState for the current session without fetching /health
fetches sibling /health and merges state and modelName
marks the session dead when sibling /health fails with no cache
returns cached state when sibling /health fails after a previous success
clearSiblingStateCache removes cached entry
sweeps cached state for sessions that drop out of the active set
buildEnrichedSessionsList includes own session and excludes daemon
buildEnrichedSessionsList returns own session in single-session mode without /health calls
session-deck-awaiting.test.ts
4
/
4
9ms
buildSessionDeck detail-view awaiting
4 / 4
renders REAL options (navigable → select_option), not hardcoded Yes/No/Always
non-navigable inline prompt → respond with the option shortcut
observed session (requestId, no options): no fabricated Allow/Deny — only PTY sessions expose real choices
awaiting but not remotely answerable (no options, no requestId): no fake action commands
session-id-resolve.test.ts
4
/
4
4ms
resolveSessionIdPrefix
4 / 4
returns exact matches untouched
restores the ESP32 31-char truncation of an observed id
leaves ambiguous prefixes unresolved
leaves unknown ids and empty input untouched
session-push-channel.test.ts
10
/
10
19ms
session push channel (real handler, fake sockets)
10 / 10
registers an ssh -L (loopback) worker as remote via the explicit remoteAttach flag
carries the session weight into the remote sessions list
does NOT register a plain local worker (loopback, no flag) — but still acks
still registers a flag-less worker from a non-local IP (pre-flag back-compat)
worker reconnect: new socket takes over, old close cannot kill the new registration or focus
session_event_up: only the registered sender may speak for a session
session_push_state overlap: a pre-reconnect socket can change neither the remote registry nor the effective push-state cache
session_push_state from a never-registered stranger socket is fully rejected
session_push_state with no remote registration keeps the plain unguarded local path
leaves non-push frames unconsumed
session-transcript-timeline.test.ts
5
/
5
7ms
lastAssistantTextFromTranscript
5 / 5
returns the last assistant text at Stop time
skips tool_result user continuations between assistant messages
returns empty when the tail is an unanswered prompt
returns empty for a missing file
skips malformed tail lines
subagent-timeline-store.test.ts
4
/
4
24ms
subagent rows in the bridge timeline store
4 / 4
keeps siblings of one fan-out that share a label and an instant
still dedups identical rows that claim no child identity
upserts a growing dispatch burst in place, past the 1s tolerance
does not shed the dispatch row ahead of ordinary tool rows
subagent-timeline.test.ts
10
/
10
10ms
SubagentTimelineTracker
10 / 10
collapses start and stop into existing compatible Timeline types
consumes child tool hooks without producing per-tool noise or controls
keeps future child notification and stop hooks out of parent control
summarizes team task completion without exposing team settings
recognizes Codex lifecycle names and preserves the provider
keeps a simultaneous fan-out distinguishable and counted
reports an explicit zero once a fan-out drains, never absence
does not pin a parent at "running" when a stop hook is lost
separates one wave from the next
reports a census change so the parent row can be rebroadcast
timebox-ble.test.ts
12
/
12
11ms
Timebox device identity
1 / 1
uses the BLE address as the device id
micro layout (Timebox 11×11)
11 / 11
returns a size²·3 RGB buffer
draws a bright dominant creature for a processing session
degrades parent-linked subagents to fixed perimeter companions
shows the device-native standby tide when no session exists
moves critical usage to a red perimeter rail without tinting identity
maps claude-code to its shaded official mark and product color
maps codex-cli to its shaded official mark and product color
maps opencode to its shaded official mark and product color
maps openclaw to its shaded official mark and product color
preserves OpenCode negative space and OpenClaw teal eyes
renders the generated Antigravity mark as a multicolor open arc
timeline-orphan-reaper.test.ts
5
/
5
6ms
BridgeTimelineStore.reapOrphanTaskStarts
5 / 5
synthesizes task_end for orphaned task_start rows only
anchors the synthetic end after the task's last row with an approx duration
closes stale orphaned chat_start rows without touching live or completed turns
anchors the synthetic chat_end before the session's next chat_start
lets a real task_end merge over the synthetic by taskId
timeline-persistence.test.ts
13
/
13
32ms
persistence is opt-in
2 / 2
writes nothing until enablePersistence is called
writes once enabled
write behaviour
5 / 5
coalesces a burst of entries into a single write
leaves no temp file behind
flushes pending entries on shutdown instead of losing them
stopPersistence is safe when nothing is pending
survives an unwritable path rather than taking the daemon down
round-trip
2 / 2
a written file reloads into an equivalent buffer
restored history is included in the next write, not truncated away
cross-implementation format contract (Swift DaemonTimelineStore)
4 / 4
writes a plain JSON array, not a wrapper object
every row carries the ts/type/raw triple both implementations require
reads a file in the shape the Swift daemon writes
treats a corrupt file as empty history rather than throwing
timeline-projection-cutover.test.ts
4
/
4
5ms
BridgeTimelineStore projection cutover
4 / 4
default OFF: every entry passes (no behavior change)
ON: drops locally-emitted chat/tool rows
ON: projected (bypass) chat/tool rows pass through
ON: task hierarchy + error rows are never suppressed
timeline-summarizer-chain.test.ts
5
/
5
7ms
summarizeResponse provider chain
5 / 5
prefers on-device Foundation Models over MLX
falls to MLX when the helper is unavailable
falls to Ollama when the helper and MLX are both down
returns null when every provider is down so the caller uses the heuristic
probes the helper once, not once per summary
timeline-task-retention.test.ts
6
/
6
346ms
BridgeTimelineStore task-row retention
6 / 6
keeps an in-flight task_start alive through FIFO overflow
evicts closed task pairs once the task cap is exceeded, oldest first
sheds tool_exec before chat_start so a tool-heavy turn survives replay
getHistoryForSession returns task rows beyond the per-session limit
getHistoryForSession canonicalizes observed session ids
loadPersistedEntries keeps task rows past the generic trim
tui-hud-entries.test.ts
16
/
16
21ms
buildHudEntries — primary anchoring
7 / 7
promotes the matching-port sibling to primary instead of appending a duplicate
patches the anchored sibling with the primary's live fields (matches macOS / Android)
uses the primary's projectName for the #N suffix grouping after anchoring
preserves the deterministic #N order when primary anchors a sibling slot
appends a synthetic primary when no sibling shares its agentType
skips the synthetic primary when a sibling shares agentType but no port match (duplicate guard)
never appends primary when agentType is daemon or openclaw
buildHudEntries — virtual OpenClaw
2 / 2
inserts a virtual OpenClaw row when gateway is available and sessions has none
does not insert a virtual row when sessions already contains an openclaw entry
buildHudEntries — hotkey eligibility (sibling-only)
1 / 1
primary and virtual rows do not consume hotkey slots
formatTaskEvalSuffix — task_end badge for each outcome class
6 / 6
renders ✓ for success
renders ✗ for fail
renders △ for partial
renders ⊘ for abandoned (manual cancel)
renders ? as score placeholder when judge has not produced a number yet
returns empty string while the eval is still pending (outcome undefined)
usage-api.test.ts
24
/
24
13ms
resolveOAuthCredentials
11 / 11
darwin: parses the Keychain blob and never reads a file
win32: reads /.credentials.json and never spawns the security CLI
win32: CLAUDE_CODE_OAUTH_TOKEN takes precedence over the file, expiresAt undefined
win32: an empty CLAUDE_CODE_OAUTH_TOKEN falls through to the file
win32: a whitespace-only CLAUDE_CODE_OAUTH_TOKEN falls through to the file
win32: a padded env token is trimmed before use
linux: honors a relocated configDir (CLAUDE_CONFIG_DIR)
win32: neither file nor env → null, no throw
win32: malformed / partial JSON → null, never throws
win32: valid token but non-numeric expiresAt → token kept, expiresAt undefined
darwin: security CLI returns null (no keychain entry) → null
buildCredSources (production wiring)
3 / 3
wires the real platform, env, and default configDir
honors CLAUDE_CONFIG_DIR when set
passes the security + file readers as UN-invoked function references (lazy thunks)
parseScopedLimits
6 / 6
keeps only model-scoped entries (drops session / weekly_all)
sorts worst-first: active ahead of inactive, then by descending percent
inactive limits sort by descending percent
falls back to kind when the model has no display_name
skips entries without a numeric percent
returns [] for non-array / missing input
fileCacheExpired — TTL slack
4 / 4
expires a cache that is a fetch-latency hair under the nominal TTL
expires at the nominal TTL and beyond
still holds a genuinely recent cache — the cross-session 429 dedupe survives
slack stays well inside the poll interval
usage-event.test.ts
33
/
33
15ms
buildUsageEvent subscription quota scoping
4 / 4
omits Anthropic quota when the model is not yet known
omits Anthropic 5h/7d quota for GLM/API-backed models
keeps Anthropic 5h/7d quota on the daemon hub even with a non-Claude aggregate model
keeps Anthropic 5h/7d quota for Claude model aliases
buildUsageEvent scoped limits
3 / 3
forwards scopedLimits for a subscription Claude model
omits scopedLimits for an API/GLM-backed model even when present
omits scopedLimits when the scoped array is empty (no phantom key)
buildUsageEvent staleness contract
5 / 5
flags usageStale when no quota numbers ride the frame (never fetched)
does not flag usageStale when fresh quota numbers are present
always puts usageStale on the wire, never omits it
keeps usageStale true when cached numbers ride a stale frame
does not force usageStale when the cost-based API-billing percent is present
buildUsageEvent Codex window normalization
10 / 10
marks an expired window stale and drops its resetsAt (no misleading "now")
leaves a live window untouched
forwards capturedAt so consumers can age the reading themselves
does NOT set `stale` on a merely-old snapshot of a live window
keeps capturedAt through the credits-only synthetic window
treats each window independently (5h expired, 7d live)
routes a weekly window arriving in Codex's primary slot to the secondary (7D) wire slot
maps exhausted credit-based plan (null windows) to a 100% primary gauge
maps an unlimited credit plan to a 0% primary gauge
leaves a partial credit balance absent (no honest percentage)
mergeRelayedSessionUsage
4 / 4
keeps the relay session counters
keeps the daemon account half authoritative — a quota-less relay cannot blank the gauge
leaves the daemon value in place for a session field the relay omits
does not mutate either input
buildUsageEvent Codex plan reconciliation
7 / 7
voids a snapshot minted under a plan the account no longer holds
keeps a snapshot whose plan still matches the account
keeps a genuine free-tier snapshot — free is a plan, not a synonym for void
keeps the snapshot when either side reports no plan (absence is no information)
emits the account tier even with no rollout at all, so a client can RETRACT
stays absent for a user with no Codex account at all
still names the plan in the subscriptions footer after voiding
usage-relay-fallback.test.ts
5
/
5
69ms
fetchUsageRelayed — direct-API fallback (regression: fa1d98c9)
5 / 5
no siblings → uses the direct API
siblings exist but the relay yields nothing → falls back to the direct API
siblings exist but only serve null usage → falls back to the direct API
sibling serves fresh usage → relay wins, direct API not called (429 prevention preserved)
stale sibling data (>5 min old) is ignored → falls back to the direct API
weather.test.ts
11
/
11
8ms
parseWeatherSettings
2 / 2
accepts lat/lon + optional place
rejects missing, malformed, or out-of-range config
wmoSummary
1 / 1
maps representative WMO codes to short words
findTodayRainWindow
3 / 3
finds the first remaining contiguous window ≥ the floor with its peak
single qualifying hour → no endHm
no qualifying hour today (tomorrow does not count) → undefined
toGlanceWeather
1 / 1
rounds temps, maps codes, and carries tomorrow + rain window
WeatherProvider
4 / 4
unconfigured → undefined without fetching
fetches once, then serves from cache inside the window
serves the stale cache on fetch failure, up to the stale-serve bound
failure with no cache → undefined (never throws)
windows-service.test.ts
8
/
8
6ms
xmlEscape
1 / 1
escapes XML metacharacters
buildScheduledTaskXml
7 / 7
produces a well-formed v1.2 task with a logon trigger
mirrors LaunchAgent KeepAlive / no-stop semantics
uses node as unquoted Command and quotes the cli.js path in Arguments
carries the network posture in Arguments — Task Scheduler has no env element
emits no trailing space when there is no posture to carry
XML-escapes special characters in the user id and paths
places both UserId fields (trigger + principal) with the same user
ws-server-auth.test.ts
3
/
3
100ms
WsServer remote auth
3 / 3
rejects a remote connection with no token
accepts the machine token
rejects any non-machine token — the machine token is the ONLY accepted credential
codex-install.test.ts
42
/
42
93ms
codex-mini-toml: applyManagedBlock
4 / 4
appends fence when absent
replaces existing fence block
apply twice is idempotent
moves Codex hook trust state out of the managed fence
codex-mini-toml: removeManagedBlock
2 / 2
leaves user content
is idempotent without fence
codex-mini-toml: hasTopLevelKeyOutsideFence
3 / 3
detects user notify key
ignores notify key inside table
ignores notify key inside fence
codex-mini-toml: hasTableOutsideFence
6 / 6
detects user [otel] table
detects user [otel.exporter] dotted table
detects array-of-table header [[hooks.Stop]]
ignores Codex hook trust state outside fence
ignores [otel] inside fence
ignores [otelfoo] (word boundary)
codex-mini-toml: hasIncompatibleHookTableOutsideFence
2 / 2
allows official lifecycle arrays to merge
rejects regular hook tables while ignoring trust state
codex-mini-toml: quoted
3 / 3
escapes backslash and double quote
escapes newline and tab
passes simple ASCII
codex-install: managedBlockBody
6 / 6
roundtrip preserves user TOML byte-for-byte (load-bearing)
matches Codex schema (lifecycle hooks + endpoints + notify dummy)
uses PowerShell command hooks on Windows
windows notify sidecar reads payload from $args and posts codex_turn_complete
falls back instead of emitting an out-of-range OTel endpoint
omits conflicting optional channels when asked
codex-install: install / uninstall (file I/O)
16 / 16
creates config with fence when file is absent
preserves user content when installing into existing config
skips when user already has [features] table outside fence
preserves user lifecycle arrays in the same TOML representation
skips when user has an incompatible regular [hooks] table
omits notify when user has top-level notify
omits OTel when user has [otel] table
omits OTel by default on Windows installs
writes the notify sidecar script on Windows and removes it on uninstall
downgrades to lifecycle-only with a warning when the sidecar write fails
skips notify on Windows when user has top-level notify (no sidecar written)
uninstall strips fence and preserves user content
uninstall is idempotent when no config exists
honours AGENTDECK_NO_CODEX_HOOKS=1 opt-out
install is idempotent (same port → no rewrite)
preserves Codex hook trust state across reinstall
opencode-install.test.ts
13
/
13
159ms
opencodePluginSource contract
6 / 6
posts the four lifecycle events the daemon pipeline understands
carries the payload fields the daemon reads
self-disables inside managed agentdeck PTYs (AGENTDECK_PORT marker)
resolves the daemon port from the canonical daemon.json candidates
bounds every network call (peer silence is a first-class signal)
separates user prompt parts from assistant response parts by messageID role
AgentDeckObserver event sequencing
2 / 2
posts one user_prompt_submit per user message, including after the turn settles
still reports a genuine second turn on a new user message
installOpenCodeHooksIfNeeded
5 / 5
writes the plugin file (creating parent dirs) and is idempotent
refreshes a stale or edited plugin file
honours the AGENTDECK_NO_OPENCODE_HOOKS opt-out
uninstall removes the plugin file and is idempotent
default path lives under the opencode plugins dir
anim-schedule.test.ts
11
/
11
11ms
D200H animation schedule
11 / 11
spans exactly one animation cycle
steps evenly and monotonically through the cycle
keeps the dash step small enough to read as motion, not as a jump
paces the loop at the Stream Deck tick so both surfaces share a tempo
gives each key its own phase without breaking the loop
is deterministic per key so a tile keeps encoding to the same bytes
identifies a rotated loop by its OWN first frame, not by deck 0
starts each key at the frame its phase offset names
separates neighbouring keys by a visible fraction of the cycle
spreads a full deck across the cycle instead of clustering
probes a frame that a moving tile cannot match by accident
deck-signature.test.ts
9
/
9
6ms
D200H deckSignature — every render-affecting change must be visible
9 / 9
is stable when nothing changed
changes when a multi-question prompt advances to the next question
changes when only the question text moves
changes when only the option labels move
changes when only the group index moves
changes when answerability flips (dead-button regression)
changes when the prompt shape flips to multi-select
changes on review badge transitions
changes on usage-only updates (quota gauges ride a separate event)
manifest-localization.test.ts
6
/
6
10ms
ulanzi manifest ↔ localization alignment
6 / 6
declares exactly one dynamic action
en.json repeats the manifest text verbatim
zh_CN.json Actions is index-aligned with the manifest
zh_HK.json Actions is index-aligned with the manifest
ja_JP.json Actions is index-aligned with the manifest
ko_KR.json Actions is index-aligned with the manifest
property-inspector.test.ts
10
/
10
30ms
ulanzi property inspector
10 / 10
is declared by the manifest and present on disk
loads the SDK Property Inspector libraries that the package ships
opens the H5 tutorial through the SDK, at a path the package contains
is self-contained — no remote asset can leave either surface half-drawn
keeps English in the markup, not only in the language files
resolves every data-localize key against every shipped language
resolves every runtime string lookup against every shipped language
keeps every language key-aligned with canonical en
preserves the {port} placeholder the script substitutes
reads the daemon only through routes that carry no secret
reconnect-supervisor.test.ts
8
/
8
12ms
ReconnectSupervisor
8 / 8
connects once on start and reports the first open without firing onReconnect
reconnects with backoff after a drop and fires onReconnect on re-open
ignores the spurious close emitted while a connect is in flight
retries via connect-timeout when an attempt neither opens nor closes
escalates the backoff ladder across consecutive failed attempts
forces a fresh connect on wake even when the socket looked open (half-open)
reconnects immediately on wake while disconnected (backoff reset)
stop() halts the watchdog so wake no longer reconnects
state-store.test.ts
6
/
6
9ms
D200H StateStore session isolation
3 / 3
does not use the global OpenClaw model while Claude focus is pending
ignores unscoped and foreign prompt options for the selected session
applies prompt options only to their correlated session
D200H StateStore — observed rows are not shadowed by the focus reply
3 / 3
keeps the live question and options when the focus reply lands
lets the roster row win even over a snapshot stored before the row arrived
still lets a managed session's live state override its roster row
bridge-client.test.ts
3
/
3
3.4s
BridgeClient — port provider
3 / 3
skips connect when provider returns null
connects once provider returns a live port
rebinds to a new port when provider value changes
device-profile.test.ts
3
/
3
4ms
Stream Deck device profiles
3 / 3
maps every documented Elgato DeviceType without conflating families
uses the larger text profile only for original 72px keys
does not coerce missing or malformed device types to Stream Deck
encoder-offline-policy.test.ts
3
/
3
7ms
renderOfflineTouchStrip — all-or-nothing 800px slice contract
3 / 3
every slice is a 200x100 canvas carrying the shared banner content
slices 0..3 offset the same banner by -index*200 (E0..E3 align into one strip)
uses a per-slice gradient id so the four panels are independent SVGs
focused-detail-state.test.ts
5
/
5
7ms
FocusedDetailState
5 / 5
does not carry an OpenClaw GLM model into a focused Claude session
drops late options from another session and unscoped legacy options
accepts options correlated to the selected session
keeps an observed session's live prompt when the focus reply lands
still lets a managed session's live state replace its detail snapshot
launcher-target.test.ts
16
/
16
11ms
launcher: runTarget fallback chain
8 / 8
opens a desktop app for an app: target
opens a browser tab for a url: target
stops at the first step that succeeds
falls through to the URL when the app is not installed
rejects when every step fails, so the dial can showAlert
rejects an unrecognized scheme rather than guessing
rejects an empty target
tolerates whitespace around chain steps
launcher: shipped defaults
2 / 2
every default target parses into runnable steps
falls back to a URL for both agents that have a desktop app
launcher: entry list and rolling
6 / 6
lists every agent in a stable order
honours a per-agent override from the Property Inspector
ignores a blank override rather than launching nothing
wraps the rolling index in both directions
never returns a negative or out-of-range index
returns 0 for an empty list instead of NaN
manifest-platform.test.ts
2
/
2
5ms
Stream Deck Marketplace platform contract
2 / 2
ships installable entries for both maintained desktop platforms
mirrors the plugin package version as X.Y.Z.0
session-slot-manager.test.ts
53
/
53
71ms
SessionSlotManager detail layout
20 / 20
re-points detail focus onto the codex fold representative when the focused thread is absorbed
exits detail view when the focused session is gone with no fold successor
folds codex companion threads by project before slot assignment
renders connected no-session list as status cards instead of text-only empty buttons
renders the OpenClaw exec approval from the row when the relay says idle
shows an OpenClaw approval as display-only when it is not deliverable
puts processing tool info before OpenClaw presets
keeps a processing status tile even before tool metadata arrives
aliases the model name on detail MODEL surfaces (status card + OpenClaw preset)
renders the MODEL tile exactly once in Claude PROCESSING detail (no duplicate)
does not duplicate MODEL on OpenClaw idle (preset + status card)
does not render a READY/idle tile while a Claude session is PROCESSING
keeps observed Claude PROCESSING free of queued task buttons
observed AskUserQuestion: options are inert without liveAnswerable, pressable with it
renders no MODEL tile for observed Codex PROCESSING on every Stream Deck layout
never renders MODEL as a key for any observed session state
does not render a STANDBY/idle tile while an OpenClaw session is PROCESSING
uses actual parser options and reserves MORE only when awaiting overflow exists
resets option paging when the question changes, not just when awaiting ends
makes observed options pressable exactly when the daemon says they are answerable
SD+ keypad relocation (Phase 2)
4 / 4
AWAITING permission renders selectable option buttons + ESC on the SD+ keypad
shows a SUGGESTED quick-send button leading the IDLE detail content on SD+
does NOT add a SUGGESTED button on classic Stream Deck (behavior unchanged)
drops the SUGGESTED button when the session leaves IDLE
SessionSlotManager list-view usage tiles
15 / 15
pins Claude 5H/7D to the last two keys of a classic Stream Deck (no Codex)
reserves 4 keys (Claude 5h/7d + Codex 5h/7d), left-aligned, when both agents report quota
hides Codex tiles when only Claude reports quota (reserve 2, not 4)
hides Claude tiles when only Codex reports quota (reserve 2 Codex tiles)
does NOT reserve usage on Stream Deck+ (encoder carries usage)
does NOT reserve usage on the Stream Deck + XL (6 dials carry usage)
pins usage to the last keys of the Stream Deck XL (no encoder, 32 keys)
isPlusFamily covers both dial-bearing families but not the plain XL
reserves NO usage keys when no quota was fed (hide-if-absent)
drops Claude tiles when its quota goes stale (hide-if-absent on stale)
fits 13 sessions on a classic deck without paging (15 keys − 2 usage)
paginates over capacity: NEXT→ at slot 12, usage at 13/14
repositions NEXT→ ahead of a 4-key usage block when paginating
pressing a usage tile resolves to refresh-usage
shows usage tiles even with zero sessions
VOICE hold-to-talk key
4 / 4
renders VOICE after the CC idle presets and maps its press to voice-ptt-begin
keeps localAction on materialized CC presets so REVIEW presses dispatch
restyles the VOICE key from daemon voice_state transitions
offers VOICE on observed Claude idle next to REVIEW (terminal-injection delivery)
SessionSlotManager detail cache ownership
3 / 3
does not carry the previous session model into the next detail view
seeds the detail readout from the session row rather than blanking it
drops the cached model when re-entering a session that has none
SessionSlotManager scoped cap vs the Codex usage keys
7 / 7
gives the key Codex vacated to the scoped cap on a free ChatGPT tier
shows Claude 5H, 7D, active Fable, and Codex weekly together on 4 reserved keys
keeps Codex windows beside active Fable and enables paging when >4 gauges exist
places an inactive cap after live Codex windows on 4 reserved keys
drops the scoped cap along with the Claude gauges when usage goes stale
shows Claude 5H, 7D, Fable, and a lone 30-day Codex window together across 4 reserved keys
shows the scoped cap for a Codex-less, Claude-only user too
streamdeck-profiles-sync.test.ts
6
/
6
10ms
bundled Stream Deck profiles in sync
6 / 6
agentdeck-sd.streamDeckProfile matches the SSOT
agentdeck-sdmini.streamDeckProfile matches the SSOT
agentdeck-sdplus.streamDeckProfile matches the SSOT
agentdeck-sdxl.streamDeckProfile matches the SSOT
agentdeck-sdplusxl.streamDeckProfile matches the SSOT
covers exactly the five shipped device families
system-dispatch.test.ts
6
/
6
13ms
system: selectBackend platform mapping
3 / 3
maps win32 to the windows backend
maps darwin to the mac backend
falls back to the mac backend on platforms the Stream Deck app does not exist on
system: setOutputVolume debounce
3 / 3
coalesces rapid rotation — only the final value commits
commits again after the debounce window settles
swallows backend failures — a bad tick must not become an unhandled rejection
system-win32-open.test.ts
5
/
5
10ms
win32: openUrl via rundll32
2 / 2
passes the URL as a single argv to rundll32, never through cmd
rejects when rundll32 fails so callers can fall back
win32: openApp via Start Apps lookup
2 / 2
passes an exact Start-menu name through the environment, not PowerShell source
rejects a missing Start-menu app so the |url: fallback chain can take over
win32: openAgentDeckAppOrGitHub
1 / 1
goes straight to the project page — there is no Windows desktop app
system-win32-volume.test.ts
14
/
14
31ms
win32 volume coprocess
14 / 14
spawns powershell with -EncodedCommand carrying the ReadLine server script
waits for READY before writing, then GET resolves from an OK line
SET and MUTE use the wire format and parse the post-state echo
an ERR line rejects only that request; the child keeps serving
serializes: the second request is not written until the first resolves
coalesces queued SETs while the child hangs — only the final value is written
kills a hung child on the first-request timeout and respawns lazily
breaker: three never-served children (incl. hung-killed-by-timeout) flip unsupported
a served child dying is NOT a strike, and resets the count
a spawn 'error' without 'exit' (ENOENT) still rejects and counts a strike
a zombie child's late buffered stdout cannot resolve the new child's request
a child exit rejects the in-flight and all queued requests
a stream 'error' event is absorbed — an unlistened stream error would crash the process
dispose writes EXIT and kills the child
usage-encoder.test.ts
28
/
28
10ms
buildClaudeUsageEncoder
4 / 4
maps the top-level 5h/7d quota to two known tanks
shows "Waiting…" before the first payload
shows "No usage data" when data arrived but the quota is absent
treats stale data as unknown (suppresses the tanks)
buildCodexUsageEncoder
4 / 4
maps codexRateLimits primary→5h, secondary→7d
falls back to a muted note when Codex reports no rate limits
shows "Waiting…" before the first payload
renders only the windows Codex actually reports (partial)
buildCodexUsageEncoder — single live window
8 / 8
shows the subscription tier and its next renewal
leaves the countdown to the gauge instead of restating it on the card
picks the ChatGPT subscription by name, never by position
drops the renewal line when the subscription has no boundary
falls back to the bare plan tier when no subscription is reported
falls back to the absolute reset instant when even the tier is unknown
adds no card at all when the stale window leaves nothing knowable
adds no side card while both windows are live
buildCodexUsageEncoder — snapshot freshness
4 / 4
says nothing extra for a fresh snapshot
THE REGRESSION: an aged snapshot is dated, and the percent survives
renders the age into the LCD even though the side card owns the reset slot
a legacy producer (no capturedAt) is unchanged
renderUsageEncoderBoth — single live window
5 / 5
keeps the lone weekly gauge in its canonical right column, side card on the left
keeps the gauge printing its own countdown, like E2 does
keeps a lone 5h gauge in the left column instead
renders the side card smaller than the gauge percentage
still renders two gauges when both windows are live
availableUsageViews
3 / 3
drops the zoom stop for a window the provider no longer reports
keeps every stop while both windows are live
leaves only the gauge and session stops when no window exists
usage-scoped.test.ts
13
/
13
11ms
sanitizeScopedLabel
2 / 2
drops control chars / newlines and collapses whitespace
is null/undefined safe
splitResetTwoLine
2 / 2
splits a two-unit countdown at the first boundary
leaves a single-unit string on one line
pickWorstScopedLimit
3 / 3
returns the worst-sorted [0] (store keeps active-first, percent-desc)
returns undefined when the snapshot is stale (a stale number reads as live)
returns undefined when there are no scoped limits
scoped gauge treatment — active vs inactive
4 / 4
an ACTIVE cap at 98% wears the critical red ramp
an INACTIVE cap at 98% wears the calm informational cyan, NOT red
the triple view mutes an inactive worst-scoped panel (no red from a 99% inactive cap)
renders the unknown tile when there is no scoped cap
scoped label on the compact gauge
1 / 1
sanitizes, uppercases and truncates the model label
scoped store merge — omitted key clears
1 / 1
an update that omits scopedLimits clears a prior set (no phantom latch)
npm-registry-visibility.test.ts
2
/
2
6ms
npm registry visibility
2 / 2
retries until a newly published version becomes visible
fails after the bounded attempt count
release-notes.test.ts
11
/
11
11ms
release notes lookup
11 / 11
npm at its declared version has an entry
streamdeck at its declared version has an entry
ulanzi at its declared version has an entry
android at its declared version has an entry
esp32 at its declared version has an entry
apple at its declared version has an entry
matches a channel named anywhere in a multi-channel heading
does not let a prefix version match a longer one
never falls back to a bare version heading
stops a section at the next heading
rejects anything that is not a delivery tag
version-policy.test.ts
5
/
5
6ms
cross-target version compatibility
3 / 3
ignores patch values and ordering within the same major.minor line
rejects different major or minor lines
requires strict numeric X.Y.Z versions
release tag identity
2 / 2
matches a release tag against that target, not the root patch
can resolve every maintained target source
setup-args.test.ts
4
/
4
2ms
parseSetupArgs
4 / 4
prints next steps by default instead of installing the daemon
--yes / -y finishes the install without a second command
--enterprise implies --yes
ignores unknown flags rather than refusing to install
brand-assets-contract.test.ts
11
/
11
8ms
canonical agent brand assets
11 / 11
keeps the Claude compatibility export on the Claude Code robot mark
claudecode.svg is the geometry used by shared renderers
codex.svg is the geometry used by shared renderers
openclaw.svg is the geometry used by shared renderers
opencode.svg is the geometry used by shared renderers
antigravity.svg is the geometry used by shared renderers
kiro.svg is the geometry used by shared renderers
mirrors every canonical path on Android and Apple vector surfaces
generates constrained-device masks directly from canonical SVG files
records the upstream source and trademark holder for every agent mark
does not retain alternate logo-source dumps
codex-freshness-rules.test.ts
8
/
8
8ms
codex freshness threshold invariants
2 / 2
is an absolute duration, not a fraction of any window
is a whole number of minutes, so every platform can print it
generated mirrors in sync
5 / 5
apple/AgentDeck/Model/CodexFreshnessRules.generated.swift matches the SSOT
android/app/src/main/kotlin/dev/agentdeck/util/CodexFreshnessRules.generated.kt matches the SSOT
emits the threshold in each platform's own unit
mirrors the plan-reconciliation rule to Swift (the other PRODUCER of the wire snapshot)
emits the same three footnote states the TS SSOT resolves
TS side still produces what the mirrors promise
1 / 1
threshold boundary matches the emitted constant
creature-layout.test.ts
19
/
19
37ms
creature-layout bands
14 / 14
returns nothing for a count of zero or less
'octopus' keeps every slot inside its X band
'cloud' keeps every slot inside its X band
'opencode' keeps every slot inside its X band
'antigravity' keeps every slot inside its X band
'octopus' emits exactly `count` slots
'cloud' emits exactly `count` slots
'opencode' emits exactly `count` slots
'antigravity' emits exactly `count` slots
'octopus' stays on one row up to its limit, then wraps
'cloud' stays on one row up to its limit, then wraps
'opencode' stays on one row up to its limit, then wraps
'antigravity' stays on one row up to its limit, then wraps
anchors each band at a distinct X center, so types read as clusters
crowd shrink
5 / 5
never grows the scale as a band fills up
honors the 0.40 hard floor even when a band is packed
shrinks a crowded row enough to cap neighbor overlap at ~half a body
gives a lone creature the band-native full scale
centers a lone creature in its band
d200h-layout.test.ts
28
/
28
32ms
usage tiles — usageKnown tri-state
8 / 8
renders a percent when the quota is known
renders a muted "—" instead of a confident 0% when unknown
defaults to known (percent) when the flag is omitted
wide slot shows "—" for both columns when unknown
wide slot shows percents when known
parseState infers usageKnown=false when no percent fields are present
parseState infers usageKnown=true when a percent is present
parseState honors an explicit usageKnown=false even with a coerced 0 percent
buildSessionDeck — daemon offline
4 / 4
renders the OFFLINE hero on the center key for a DISCONNECTED state
makes EVERY key launch the companion app while offline
does not show OFFLINE / launch when the daemon is connected
shows the session list (not OFFLINE) when state is disconnected but sessions exist
buildSessionDeck — selected session isolation
1 / 1
does not render another agent model while selected-session focus is pending
buildSessionDeck — per-model scoped caps
3 / 3
surfaces the worst ACTIVE scoped cap with the critical ramp
renders an INACTIVE scoped cap muted (informational cyan), never the ramp
parseState carries scopedLimits through
usage tiles — Codex snapshot freshness
4 / 4
prints the countdown, undimmed, for a fresh snapshot
THE REGRESSION: an aged snapshot keeps its % but is dimmed and dated
an ended window still reads "stale", not an age
a legacy producer (no capturedAt) renders exactly as before
usage tiles — scoped caps and the three-key strip budget
4 / 4
emits at most ONE scoped tile, however many caps arrive
documents the cost: the scoped tile takes the slot Codex 5H used to hold
sanitizes and truncates the API label before it reaches SVG text
renders no scoped tile when there are none
VOICE tile — hold-to-talk contract
4 / 4
tells the user to hold, not to tap
reads as listening while the daemon captures
still declares the wire commands the layout is responsible for
goes inert only while transcribing
d200h-observed.test.ts
14
/
14
17ms
D200H observed session detail
14 / 14
idle: REVIEW + VOICE are the actionable cells — no fake prompts, inert STOP
idle while recording: the VOICE tile flips to a stop toggle
processing: soft STOP is the only action; no queued task or REVIEW mid-turn
processing: last review verdict stays visible as an inert badge
processing + stopRequested: STOP replaced by STOPPING tile, no queue presets
awaiting + requestId: exactly ALLOW/DENY permission_decision cells
awaiting WITHOUT requestId: answer-in-terminal only, never fabricated Allow/Deny
AskUserQuestion options are visible but inert without a live-answer host
liveAnswerable turns the same options into select_option presses
codex observed idle: REVIEW only — steering stays inert (notify-only hooks)
codex observed processing: nothing actionable — REVIEW waits for turn end
opencode observed idle: inject-now presets via session_command (plugin queue)
opencode observed permission gate: ALLOW/DENY carry the ocperm requestId
managed sessions keep the original idle quick-actions (regression)
d200h-openclaw-approval.test.ts
4
/
4
16ms
D200H — OpenClaw exec approval
4 / 4
renders the real command and its decisions, not the global idle state
every decision is a pressable select_option carrying its own index
keeps the relayed event authoritative when IT is the one with the prompt
an idle gateway row still reads as idle
format-utils.test.ts
58
/
58
19ms
formatDurationSec
4 / 4
formats sub-minute spans as seconds
formats minute spans, dropping a zero-second remainder
formats hour spans, dropping a zero-minute remainder
clamps negatives to zero
formatAntigravityPlanShort
6 / 6
shortens "Google AI Pro" to "AGY Pro"
shortens "Google AI Ultra" to "AGY Ultra"
strips an "Antigravity " prefix
returns undefined for blank/absent input
is idempotent on already-shortened values
collapses a bare "Google AI" to "AGY"
adjustUsagePercent
11 / 11
returns undefined when percent is null
returns undefined when percent is undefined
returns percent unchanged when resetsAt is null
returns percent unchanged when resetsAt is undefined
returns percent unchanged when resetsAt is in the future
returns 0 when resetsAt is in the past (window expired)
returns 0 when resetsAt equals now (edge case)
handles invalid date string gracefully (returns percent)
handles empty string resetsAt (returns percent)
returns percent unchanged when resetsAt is far in the past (>1h)
still returns 0 just after the 1h threshold boundary
isCodexWindowStale
6 / 6
returns false when resetsAt is undefined
returns false for an invalid date string
returns false when resetsAt is in the future
returns false when resetsAt is in the past but within the grace window
returns true when resetsAt is past beyond the grace window
honors a custom grace
codex snapshot freshness
14 / 14
codexSnapshotAgeMs › returns undefined for missing / unparseable stamps
codexSnapshotAgeMs › measures age against the supplied clock
codexSnapshotAgeMs › clamps a future stamp to zero rather than going negative
isCodexSnapshotAged › is false when there is nothing to judge
isCodexSnapshotAged › is false for a snapshot inside the threshold
isCodexSnapshotAged › is false exactly at the threshold, true past it
isCodexSnapshotAged › honors a custom max age
formatSnapshotAge › rounds DOWN so the label never overstates freshness
formatSnapshotAge › returns undefined when there is nothing to say
codexUsageFootnote › says nothing for a live window with a fresh snapshot
codexUsageFootnote › THE REGRESSION: a 4h-old snapshot behind a 6-day-out weekly reset is aged
codexUsageFootnote › an ended window outranks age — "stale", not an age
codexUsageFootnote › says nothing when there is no window at all
codexUsageFootnote › says nothing for a live window with no capture stamp (legacy producer)
formatResetTime
7 / 7
returns "now" when resetsAt is in the past
returns undefined for null input
returns minutes-only for < 1h remaining
returns hours and minutes for < 24h remaining
returns days and hours for >= 24h remaining
omits minutes when exactly on the hour
passes through pre-formatted strings (no T)
codexSnapshotMatchesAccountPlan
3 / 3
voids a snapshot minted under a plan the account no longer holds
keeps a matching snapshot, free included — free is a plan, not a synonym for void
treats an unknown tier on either side as no information, never as licence to void
isCodexFreePlan
2 / 2
recognises the free tier regardless of case or padding
is false for every paid tier and for an unknown one
scopedLimitClaimsUsageKey
2 / 2
claims a key when a scoped cap is present
claims nothing when there is no cap
codexWindowsBeside
3 / 3
retains all Codex windows beside the scoped cap
leaves Codex untouched when no cap claims a key
returns empty when Codex reports nothing
idotmatrix-identity.test.ts
12
/
12
8ms
BLE UUID normalization
3 / 3
expands the short advertisement forms onto the Bluetooth base UUID
left-pads a short form that dropped its leading zeroes
leaves an unrelated 128-bit UUID alone
iDotMatrix discovery predicate
6 / 6
matches the iDotMatrix and iPixel name families case-insensitively
does not match unrelated peripherals
accepts a user-configured prefix for the next rebrand
identifies an unnamed peripheral by its advertised service UUID
still identifies a named panel that keeps the service out of its advertisement
rejects a peripheral with neither signal
generated mirrors in sync
3 / 3
apple/AgentDeck/Daemon/Modules/IDotMatrixIdentity.generated.swift matches the SSOT
bridge/src/idotmatrix/identity_generated.py matches the SSOT
emitters embed the SSOT literals (sanity on the emitters themselves)
llm-settings.test.ts
10
/
10
13ms
llm-settings
10 / 10
returns defaults when settings.json is missing
reads llm.mlx pin from settings.json
treats "qwen3-30b" and "default" as placeholders (unpinned)
falls back to apme.judge.model when llm.mlx is absent
strips /chat/completions suffix from legacy endpoints
prefers llm.mlx over apme.judge when both set
resolveMlxModel: pin > probe > fallback
pickMlxModel: 4-layer priority (pin > fallback > first > null)
mlxChatUrl reflects endpoint setting
caches result for TTL window
mdns-identity.test.ts
7
/
7
5ms
sanitizeMdnsLabel
2 / 2
keeps a hostname greppable and collision-free
bounds the label so the whole instance name stays inside DNS-SD limits
mdnsUserTag
2 / 2
is stable, short, and never the account name
separates two accounts on one machine
buildMdnsInstanceName
2 / 2
names host, user and port — the three ways two daemons can differ
degrades to the old shape rather than emitting a malformed name
generated Swift mirror
1 / 1
matches this source
model-provider.test.ts
8
/
8
10ms
modelProvider
2 / 2
names the provider behind every id shape the wire actually carries
answers unknown rather than guessing — the polarity is the point
offHarnessProvider
4 / 4
badges the real case: Claude Code answering from z.ai
says nothing when the harness is where it belongs
never turns two unknowns into a claim
leaves multi-provider harnesses alone
generated mirrors
2 / 2
are in sync with this source
carry every provider, so no mirror can silently drop one
no-side-effects.test.ts
4
/
4
23ms
shared is side-effect free
4 / 4
finds source files to check
has no top-level executable statements
performs no import-time I/O, timers, or global mutation
declares sideEffects: false so bundlers actually act on this
observed-agent-rules.test.ts
5
/
5
108ms
observed session ids
2 / 2
strips every advertised prefix, Kiro included
keeps the regex and the list from disagreeing about which agents exist
observed tool_exec suppression
1 / 1
covers every observed agent whose tool rows are a firehose
generated mirrors
2 / 2
match this source
leaves no hand-written copy of the list anywhere in the app sources
openclaw-approval-rules-sync.test.ts
4
/
4
6ms
generated mirror in sync
4 / 4
apple/AgentDeck/Daemon/Gateway/OpenClawApprovalRules.generated.swift matches the SSOT
the Swift mirror carries every decision the TS union declares
neither side ever offers the plain "allow" the Gateway rejects
the Swift mirror reads the nested request, not flat fields
openclaw-approval.test.ts
18
/
18
8ms
parseExecApprovalRequest
12 / 12
reads the command out of the nested request
never renders `ask` as if it were the question
carries cwd and warning as supporting detail
offers exactly the decisions the request allows
drops allow-always when the policy withholds it
honors unavailableDecisions on top of the allowed set
always keeps a deny — a prompt you can only accept is not a prompt
still produces an answerable prompt when no command was reported
falls back to flat fields if a Gateway ever inlines them
falls back to argv when only that is present
returns null only when there is no usable id
carries the expiry so a stale prompt can be swept
answering
6 / 6
maps an option index to the decision that option carries
refuses an out-of-range index instead of guessing
accepts shortcuts, decision names, and the y/n/a spellings
never turns an unrecognized press into an approval
refuses "always" when the request forbids allow-always
classifies which decisions let the command run
pairing-code-rules-sync.test.ts
12
/
12
9ms
generated mirrors in sync
5 / 5
apple/AgentDeck/Model/PairingCodeRules.generated.swift matches the SSOT
android/app/src/main/kotlin/dev/agentdeck/net/PairingCodeRules.generated.kt matches the SSOT
emitters embed the SSOT constants (sanity on the emitters themselves)
the Swift mirror emits every outcome the TS union declares
Kotlin stays a client mirror — no evaluator, no HTTP statuses
status contract the mirrors must agree on
7 / 7
accepted answers 200 in TS and in the Swift mirror
no-window answers 401 in TS and in the Swift mirror
expired answers 410 in TS and in the Swift mirror
exhausted answers 429 in TS and in the Swift mirror
mismatch answers 401 in TS and in the Swift mirror
malformed answers 400 in TS and in the Swift mirror
the Swift mirror decides expiry before it reads the code
pairing-code.test.ts
23
/
23
10ms
normalizePairingCode
5 / 5
accepts a bare code
accepts the separators a human or a formatted display introduces
rejects anything that is not exactly the code length
rejects non-strings rather than coercing them
isPairingCode only accepts the canonical form
formatPairingCode
3 / 3
groups the code in half for reading aloud
normalizes before grouping, so formatting is idempotent
passes through anything it cannot parse instead of mangling it
evaluatePairingRedemption
11 / 11
accepts the right code and closes a single-redemption window
keeps a multi-device window open until its last credential
accepts a code that arrives with the separators it was displayed with
answers 401 with no window, so a closed daemon looks like an unknown route
decides expiry before it looks at the code
treats the expiry instant as closed, not as the last open millisecond
burns one attempt per well-formed wrong code and closes on the last one
refuses a window whose attempts are already spent, even for the right code
refuses a window with no credentials left
does not spend an attempt on a malformed submission
gives an attacker at most PAIRING_MAX_FAILED_ATTEMPTS guesses per window
pairingWindowSecondsRemaining
2 / 2
rounds up so a countdown never shows 0 while the window is still open
floors at zero for an expired or absent window
constants
2 / 2
keeps the code long enough that the attempt cap is the real defence
keeps the window short enough to be an operator action, not a mode
pricing.test.ts
11
/
11
5ms
isLocalModel
5 / 5
accepts the judge tag the runner emits for Foundation Models
accepts the bare Foundation Models spellings
accepts raw ids as a local server advertises them
accepts the explicit local prefixes
does not claim hosted models are local
local models cost nothing and say so
3 / 3
prices local judge tags at $0
reports them as priced, not as unpriced
groups them under the local provider
normalizeModelId
3 / 3
strips hosted provider prefixes
keeps the org segment of a local id
still prices a hosted model after normalization
protocol-generated-sync.test.ts
12
/
12
6ms
generated protocol artifacts in sync
12 / 12
generated/protocol/bridge-event-schema.json matches the SSOT
generated/protocol/plugin-command-schema.json matches the SSOT
generated/protocol/gateway-frame-schema.json matches the SSOT
generated/protocol/BridgeEvent.swift matches the SSOT
generated/protocol/PluginCommand.swift matches the SSOT
generated/protocol/GatewayFrame.swift matches the SSOT
generated/protocol/BridgeEvent.kt matches the SSOT
generated/protocol/PluginCommand.kt matches the SSOT
generated/protocol/GatewayFrame.kt matches the SSOT
generated/protocol/AgentCommand.swift matches the SSOT
generated/protocol/AgentCommand.kt matches the SSOT
shared/src/command-builders.ts matches the SSOT
session-deck-usage.test.ts
22
/
22
36ms
buildSessionDeck list-view usage tiles
17 / 17
pins 5H/7D to the bottom strip and wires them to query_usage
leaves the upper grid and the clock-side keys entirely to sessions
fills the strip from the right, freeing the leftmost key for a session
does not reserve usage slots when showUsage is off (regression: full grid)
frees the usage slots (no residue) when usage is unknown
frees only the Claude slots when Codex-only usage is present (partial)
fits sessions into the 13 non-usage keys without paging
paginates when sessions exceed capacity, NEXT→ sits on the last free key
page 2 keeps the usage tiles pinned and shows the remainder
shows usage even with zero sessions
caps the strip at 3 keys, dropping the 4th tile (Claude prioritised)
renders only the Codex window whose datum exists
labels the Codex weekly window "7D" when it arrives in the primary slot (secondary null)
shows a credits tile when Codex reports a credit-based plan (null windows)
renders ∞ for an unlimited-credits Codex plan
falls back to trailing keys on a tiny deck where the strip is not placed
never starves the only key on a 1-key deck (usage yields to the session)
buildSessionDeck scoped cap vs the third usage key
5 / 5
gives the key Codex vacated to the scoped cap on a free ChatGPT tier
lets an ACTIVE cap TAKE the Codex key — it is the limit that binds
leaves the first Codex window standing when Codex reports two
never lets an INACTIVE cap displace a live Codex window
shows nothing but Claude when neither Codex nor a scoped cap exists
session-slot-anim.test.ts
12
/
12
18ms
session slot animation cycle
9 / 9
returns to its starting phase after one cycle (processing, active=false)
returns to its starting phase after one cycle (processing, active=true)
returns to its starting phase after one cycle (awaiting_permission, active=false)
returns to its starting phase after one cycle (awaiting_permission, active=true)
returns to its starting phase after one cycle (idle, active=false)
returns to its starting phase after one cycle (idle, active=true)
animates the processing border without an explicit start frame
keeps a supplied start frame as a phase offset, not a freeze
leaves an unfocused idle tile static — it must not be encoded as a loop
tile identity is byte-stable
3 / 3
renders the same quiet slot identically every time
renders the same empty slot identically every time
emits no random-looking ids from any session-slot renderer
session-utils.test.ts
46
/
46
25ms
OpenClaw visibility SSOT
2 / 2
isOpenClawSessionActive is true only when gatewayConnected
hasOpenClawSession detects an emitted openclaw session
agentTypeRank
1 / 1
ranks known agents stably, with Kiro after the existing agent set
naturalLabelCompare
2 / 2
orders numeric chunks naturally (Agent 2 before Agent 10)
treats undefined as empty string
sortSessions
6 / 6
places openclaw before claude-code regardless of project
sorts by project name within the same agent type
sorts numbered project names naturally (#2 before #10)
breaks ties on startedAt ascending (oldest first)
falls back to id natural-compare when startedAt ties to the same ms
does not mutate the input array
sessionWeight
5 / 5
returns the weight when it is a finite number (incl. negatives and 0)
collapses missing / null / non-finite weights to 0
accepts both documented bounds unchanged
clamps out-of-range values to the documented bounds
integer-truncates fractional values so all platform mirrors agree
sanitizeWeightForWire
2 / 2
drops non-numbers and non-finite values to undefined (wire omits the key)
passes in-range integers and clamps/truncates the rest
sortSessions weight comparator safety
1 / 1
orders opposite-sign extreme in-range weights without overflow tricks
sortSessions weight override
6 / 6
orders by weight ascending: negatives, then unweighted/0, then positives
treats an unweighted session the same as weight 0
weight beats agentType — a weighted claude-code sorts before an unweighted openclaw
within the same weight, existing ordering (agentType → project → startedAt → id) still applies
maps terminal tab order onto the deck: tab N (weight N) lands in slot N
does not change ordering when no session has a weight (backward compatible)
foldCodexSessionsForDisplay
4 / 4
folds Codex CLI and Codex App separately even with the same project
never folds same-project Codex sessions pinned to distinct weights
keeps distinct-weight sessions unfolded across state changes
still folds same-project Codex sessions when both are unweighted or share a weight
assignDisplayNames
4 / 4
passes single sessions through without #N suffix
adds #1/#2 suffixes for duplicate (project, agentType) tuples in input order
numbers same project across different agentTypes independently
produces the same #N assignment as a deterministic sort + display pipeline
normalizeProjectForGrouping
2 / 2
strips path and trailing #N suffix
handles empty/undefined
projectGroupKey
5 / 5
exact match groups
long multi-token stems fuse
delimiter extension fuses ("foo…" + "foo…-1")
short sibling projects stay separate
stems with fewer than 2 delimiters stay separate
groupSessionsByProject
3 / 3
clusters same-stem worktrees and keeps singletons flat
groups duplicate sessions of the same project (#N suffixed)
preserves input order within groups
rawSessionId / sameSession
3 / 3
strips every observed agent prefix
leaves managed and already-bare ids alone
sameSession bridges the two keying forms
session-weight-rules.test.ts
6
/
6
6ms
session weight range invariants
2 / 2
range is symmetric around the neutral band and fits every platform integer
sessionWeight clamps exactly to the SSOT bounds
generated mirrors in sync
3 / 3
apple/AgentDeck/Model/SessionWeightRules.generated.swift matches the SSOT
android/app/src/main/kotlin/dev/agentdeck/net/SessionWeightRules.generated.kt matches the SSOT
emitters embed the SSOT literals (sanity on the emitters themselves)
self-contained hand-port copies
1 / 1
D200HLayoutModel.swift carries the same weight-range literals
state-transitions.test.ts
8
/
8
15ms
generated mirrors in sync
4 / 4
apple/AgentDeck/Daemon/Core/StateTransitions.generated.swift matches the SSOT
every daemon-only generated file carries the macOS guard
emits a Swift case name for every state, with a rawValue only where they differ
carries each transition note into the mirror
transition table invariants
4 / 4
is deterministic — no two rows match the same (from, trigger, source)
leaves AWAITING_* without a wall-clock backstop
gives every AWAITING_* state a hook-driven exit
every row targets a real state and a known source
subagent-activity.test.ts
4
/
4
4ms
deriveSubagentActivity
4 / 4
pairs summarized starts and completions under their parent session
uses team completion rows only as a recent completion pulse
ignores ordinary tools and expires orphaned starts
sorts history rows before pairing them
svg-renderers/model-alias.test.ts
7
/
7
5ms
aliasModelName
3 / 3
shortens claude family-major-minor
drops trailing date suffix on claude releases
passes gpt and unknown strings through unchanged
formatModelEffort
4 / 4
returns aliased model when no effort to show
appends non-default effort when it fits
truncates aliased model name to fit budget with effort suffix
returns empty string for missing model
terrarium-rules.test.ts
5
/
5
4ms
terrarium rules invariants
2 / 2
floor-rest clear anchor stays left of the crayfish claws
rest strips sit above the crayfish, below mid-water
generated mirrors in sync
3 / 3
apple/AgentDeck/Terrarium/TerrariumRules.generated.swift matches the SSOT
android/app/src/main/kotlin/dev/agentdeck/terrarium/TerrariumRules.generated.kt matches the SSOT
esp32/src/ui/terrarium/terrarium_rules_generated.h matches the SSOT
text-sanitize.test.ts
5
/
5
5ms
stripUnsafeText
4 / 4
strips ANSI CSI sequences but keeps the visible text
strips OSC sequences and bare ESC forms
strips XML-invalid control characters but keeps tab/newline
strips lone surrogate halves but keeps full emoji
escSvgText
1 / 1
entity-escapes after stripping
timeline-summarizer.test.ts
13
/
13
5ms
extractTopicHintWithKind — Korean polite-closer robustness
7 / 7
returns the cleaned topic when the response opens with "네, …"
returns the original (kind=fallback) when the entire response is just a polite closer
returns null for empty / very short text (still)
skips lone heading markers and code fences
a heading WITH text becomes the topic (heading is the title)
strips list bullet markers
extractTopicHint convenience returns just the hint string
promptSnippetFallback
6 / 6
returns first sentence trimmed
returns whole string when no sentence terminator
truncates to maxLen with ellipsis
cuts at first newline if no sentence terminator
returns null for empty / very short input
handles Korean prompts
timeline-task-display.test.ts
15
/
15
8ms
timelineIsMeaningfulTaskTitle
2 / 2
rejects empty and auto-minted numeric titles
accepts real titles
timelineTaskClosure
2 / 2
finds the matching task_end by taskId
ignores mismatched ids and non-headers
timelineShouldRenderTaskRow
7 / 7
never renders task_end standalone — including interrupted reaper rows
passes non-task rows through
hides bare "Task N" headers with no eval payload (open or interrupted)
shows headers with meaningful titles
shows judged tasks via the closure eval payload
_empty category hides regardless of source
a non-_empty closure category alone qualifies as eval payload
timelineTaskHeaderDisplay
4 / 4
folds closure label, badge fields, and closedAt into the header
keeps a meaningful own title over the judge summary
open task: not closed, no closure text, falls back to own raw
closedAtMs falls back to the closure ts when endedAt is absent
timeline-task-hierarchy.test.ts
47
/
47
16ms
deduplicateEntry — task hierarchy
4 / 4
always adds task_start even with identical raw within 8s
always adds task_end with same boundarySignal back-to-back
always adds task_milestone even with identical raw within 8s
still dedupes ordinary chat_start within 8s
timelineIconKey
5 / 5
maps task entries to "task"
maps tool_request status to success/error/awaiting
chat_start in flight is "running"; chat_end is "success"
error → error; user_action → user; memory_recall → memory
every key has an e-ink glyph of constant 4-char width
isInFlightTask
6 / 6
task_start without matching task_end is in flight
task_start whose task_end (same taskId) appeared is finished
mismatched taskId on task_end does not close it
task_start without taskId is never considered in flight
non-task_start entries are never in flight
task_start older than the staleness cap is no longer in flight
isRotatingEntry
10 / 10
chat_start without a timestamp rotates (icon-key running, legacy rows)
fresh chat_start rotates
chat_start older than the age cap stops rotating
chat_start with a later same-session completion stops rotating
chat_start superseded by a newer same-session prompt stops rotating
other sessions' completions do not stop a running chat_start
orphan task_start rotates via in-flight predicate
closed task_start does not rotate
static rows do not rotate
eval_result and task_end never rotate
parseTimelineMarkdown
9 / 9
returns single text line for plain text
parses headings 1-6 with required space
parses bullets and numbered lists
handles code fence — verbatim lines, not interpreted
blank line → blank kind
parses tables with header separator
parses tables without separator (no header)
table block ends at first non-table line
quote lines parse
parseInlineSpans
9 / 9
returns empty for empty string
returns single plain span for plain text
parses **bold**
parses *italic* (single star)
parses `code` inline
parses [text](url) link
unclosed ** falls back to plain
multiple spans in one line
first-match-wins: ** consumed before * (no double-italic split)
prepareMarkdownDetail
4 / 4
preserves markdown markers (chat-response detail goes to client)
still filters system JSON blobs
collapses 3+ blank lines but keeps double-blank paragraph break
contrasts with cleanDetailText which strips markdown (non-chat path)
voice-ptt.test.ts
8
/
8
6ms
Stream Deck VOICE hold lifecycle
5 / 5
maps key-down plus a real hold to begin then end
cancels a key-clack tap shorter than the minimum hold
cancels a held key that disappears during a page or profile switch
ignores lifecycle events from another action instance
maps terminal PTT actions to daemon wire commands
cancelActive — releases that can never arrive
3 / 3
cancels the open hold without knowing which key owns it
is a no-op when nothing is held, so a stray socket close sends nothing
does not fire twice for one hold
voice-reply-digest.test.ts
15
/
15
11ms
spokenDigest parity cases
7 / 7
first sentence only, not the whole paragraph
explicit summary label wins over the opening sentence
bare summary label takes the line after it
a heading-only first line is skipped for the prose under it
code fences never get recited
a version number does not end the sentence
nothing speakable stays empty
spokenDigest
6 / 6
reads one sentence out of a long answer, not the whole thing
keeps a summary line even when it is the last thing in the reply
does not treat prose that merely opens with a label word as a summary
cuts an over-long single sentence on a word boundary
honours full: true for callers that opted back in
stays empty when there is nothing to say
speakableReply
2 / 2
keeps the whole readable answer
caps at the monologue ceiling
▣
Android
PASS
JUnit + Robolectric · 29 files · 375 tests
MainActivityOverrideRecoveryTest
5
/
5
5ms
a timed-out startup read is distinguishable from genuine defaults
recreate when the user changes the panel override
expired startup read recovers a persisted show-anyway choice
no recreate when storage matches what the instance was built from
pressing show anyway takes effect through the same comparison
MainActivityScreenPolicyTest
6
/
6
1ms
LCD releases keep-screen-on for host full-off sleep
LCD releases keep-screen-on when keep-awake is disabled
LCD releases keep-screen-on for legacy sleep event without dim config
LCD stays on for minimum-brightness dim mode
LCD stays on when host disables downstream dimming
e-ink stays on during host full-off sleep
data.DashboardOrientationTest
3
/
3
1ms
defaults keep e-ink fixed and tablets auto-rotating
legacy unspecified preference still counts as auto
manual rotate toggles fixed modes from auto using current posture
net.AndroidDashboardRegistrationTest
3
/
3
46ms
repeated identity does not duplicate registration churn
disconnected change is retried when the socket opens
connected profile change publishes new wire kind without reconnect
net.AutoConnectRulesTest
6
/
6
17ms
the canonical port is preferred but not required
the first loopback re-probe waits, and later ones back off
the backoff is capped, and never inverts or overflows
only the daemon hub is dialable
no daemon means no dial
a negative miss count is treated as the first probe
net.BridgeDiscoveryTest
2
/
2
7ms
ws url uses primary host and preserves pairing token
fallback url is absent without a fallback host
net.PairingCodeClientTest
14
/
14
4.9s
an unexpected status is reported rather than guessed at
normalize accepts a bare code
a missing attempt count degrades to unknown, never to zero
410 and 429 mean the window is gone, not that the code was wrong
normalize rejects anything that is not the code length
constants match the daemon's
200 without a token is a daemon fault, not a pairing
normalize accepts the separators the code was displayed with
format groups the code the way the CLI prints it
401 mismatch is a retryable wrong code, and carries the budget
200 with a token pairs, using the endpoint we actually reached
401 no-window sends the user to the Mac instead of retyping
400 malformed is retryable and spends none of the budget
401 with no body at all is treated as no window
net.PairingCredentialTest
26
/
26
16ms
a live connection to another endpoint is left alone
an endpoint that closed us 4001 is not redialled
with nothing refused the last error stands
a tokenless url persists when nothing better is stored
a refused endpoint is dialled again once we hold a credential for it
paired endpoint inherits its stored credential
token extraction treats empty as absent
default port matches an explicit default port
a refusal is remembered per endpoint, not globally
the advice a camera-less reader is given is one it can actually take
an explicitly credentialed discovery wins
pairing is still offered to a device riding the USB tunnel
different host or port are different daemons
a tokenless url for a different daemon still persists
pairing is not offered to a device already on the LAN
a refusal outranks whatever the last attempt reported
the USB attempt is not preempted while it is still in flight
the LAN endpoint is dialled once nothing else is in flight
unparseable inputs never compare equal
credential is not part of a daemon's identity
a tokenless url may not displace a stored credential for the same daemon
localhost and blank are never persisted
every refused spelling of one daemon stays refused
a credentialed url always persists
no stored credential leaves the discovered url untouched
a different daemon never inherits another endpoint's credential
net.ProtocolTest
34
/
34
1.1s
parse state_update with all permission modes
parse connection connected with sessionId
parse sessions_list
PluginCommands selectOption generates valid JSON
parse timeline_event with fractional timestamps
parse button_state
PluginCommands utility with and without value
parse invalid json returns null
parse timeline_history
parse timeline_event upsert
parse state_update with processing state and tool info
PluginCommands respond escapes special characters
parse display_state with dim instruction
PluginCommands respond generates valid JSON
parse state_update with permission options
parse connection disconnected
parse missing type returns null
parse deck_slot_map
parse usage_update with extra usage
parse state_update with ollama status
PluginCommands navigateOption
parse state_update with agent capabilities
parse unknown type returns null
parse user_prompt
BridgeTimelineEntry converts to TimelineEntry
parse state_update with model catalog
parse state_update ignores unknown fields
parse state_update with idle state
parse display_state sleep and wake
parse voice_state
parse encoder_state
PluginCommands interrupt and escape
parse usage_update with rate limits
parse timeline_event
service.PanelPolicyTransitionManagerTest
3
/
3
1ms
installing the active panel policy is a no-op
eink to lcd restores old effects before applying and re-evaluating
lcd to eink uses the same coherent transition
state.SessionMetricsTest
8
/
8
9ms
reset clears all metrics
onMessageReceived updates lastMessageAt
onConnected sets connectedSince
onDisconnected clears connectedSince
clean reconnect after disconnect does not increment
initial state has no connection
reconnect increments reconnectCount when still connected
onMessageReceived increments count
state.TimelineDisplayScenarioTest
13
/
13
11ms
synthetic response-less turn is hidden completely
multi-agent dashboard timeline projects meaningful session rows
progress chat_response and progress chat_end are hidden
chat_end is hidden when chat_start already represents a response-less turn
codex tool entries are suppressed from device timeline
synthetic chat_start is suppressed once completion arrives
task notification chat_start is suppressed
chat_end is hidden when chat_response already represents the same turn
answered turn is not folded
task_end boundary blocks fold
still-open queued prompts are not folded
queued prompt folds into next turn
same timestamp summaries stay separate by agent and project
state.TimelineStoreTest
63
/
63
36ms
addEntry drops openclaw placeholder with status-only detail
timelineDisplayGroups hides model_call after model_response in same run
deriveSubagentActivity ignores ordinary tools and expires orphaned starts
addEntry allows same type+summary after 5s
addEntries filters otel noise from bulk replay
TimelineSessionFilter label prefers project then friendly agent
clear empties the store
addEntry keeps coalesced codex subagent lifecycle
upsertEntry updates existing entry within 1s tolerance
addEntry drops opencode tool_exec so a tool-heavy turn does not flood
upsertEntry without taskId falls back to ts-window match
timelineDisplayGroups keeps in-flight chat_start until completion
groupConsecutive merges a meaningful chat_start with its response into one turn
addEntry drops antigravity tool_exec forward-compat
groupConsecutive empty list returns empty
addEntry drops openclaw placeholder tool rows
timelineLifecycleBounds pairs response with prior start
upsertEntry adds new entry if no match
addEntry stores entry
matchesTimelineFilter keeps sessionless OpenClaw rows under gateway
matchesTimelineFilter falls back to project plus agent for legacy sessionless rows
groupConsecutive does not merge tool_request across sessions
groupConsecutive groups same summary within 60s
matchesTimelineFilter canonicalizes observed session prefixes
timelineDisplayGroups keeps independent sessions visible
updateLastOfType modifies the last matching entry
upsertEntry refuses to insert otel noise via add fallback
addEntries merges and deduplicates
addEntry drops openclaw placeholder with arbitrary future status
upsertEntry preserves existing detail if new detail is null
deriveSubagentActivity pairs starts and completions per parent
addEntry keeps real openclaw tool rows
groupConsecutive splits tool_request after 10s gap
addEntries sorts by timestamp
upsertEntry preserves existing summaryKind when new entry omits it
groupConsecutive does not merge chat_end across projects
matchesTimelineFilter with sessionId ignores project fallback
upsertEntry preserves timeline attribution
turn merge looks past interleaved rows from other sessions
addEntry allows different type within 5s
addEntry drops openclaw placeholder with failed status
groupConsecutive single entry
addEntry deduplicates within 5s window
upsertEntry propagates summaryKind progression heuristic to llm
timelineDisplayGroups collapses synthetic chat_start once response arrives
groupConsecutive splits different types but folds the turn completion
addEntry allows different summary within 5s
groupConsecutive groups chat_end by type only within 60s
addEntry drops codex otel low-signal tool noise
addEntry caps at MAX_ENTRIES
groupConsecutive does not merge distinct run ids on same session
groupConsecutive folds a whole chat_start-response-end turn into one row
timelineDisplayGroups keeps in-flight start when later completion has distinct run id
addEntry drops codex tool_exec from normal codex session
addEntry keeps openclaw placeholder summary when detail has content
groupConsecutive requires same summary for other types
addEntry normalizes openclaw cron model_call prompt dump
matchesTimelineFilter matches by sessionId
updateLastOfType no-op if type not found
turn merge never crosses a newer same-session chat_start
groupConsecutive groups tool_request within 10s
addEntry drops codex tool_exec even when raw is meaningful
upsertEntry merges task_end by taskId beyond the 1s tolerance window
state.TimelineTaskHierarchyTest
39
/
39
26ms
OpenClaw NO_REPLY polling responses are hidden from display projection
chat_start with a later same-session completion stops rotating
mismatched taskId on task_end does not close it
chat_start older than the age cap stops rotating
summary is redundant with detail for standalone response prefix truncation
OpenClaw automated polling chat starts are hidden from display projection
stripMarkdownInline drops markers including table syntax
interrupted reaper closure pair leaves no visible row
summary is not redundant for merged prompt-response turn
detail redundancy fires when prefix matches summary
detail redundancy does not fire when content is genuinely new
same project two sessions are not the same context anymore
closed task_start does not rotate
non task_start entries are never in flight
fresh chat_start rotates via icon-key running
task_start whose task_end (same taskId) appeared is finished
static rows do not rotate
session_end task boundary is hidden from display projection
stripMarkdownInline preserves non-table pipes
task_start older than the staleness cap is no longer in flight
task_start without matching task_end is in flight
chat_start superseded by a newer same-session prompt stops rotating
idle_gap task boundary is hidden from display projection
task entries never group with each other
iconKey resolves tool_request status to success error awaiting
taskId is the strongest grouping key
OpenClaw LINE notification failures stay visible
promote informative lead skips generic outcome paragraph
other sessions completions do not stop a running chat_start
iconKey resolves to Task for task entries
judged closure makes a bare header visible with the summary as title
eval_result and task_end never rotate
stripMarkdownInline handles single-cell table row
markdown parser parity with shared - basics
task_start without taskId is never considered in flight
orphan task_start rotates via in-flight predicate
markdown parser code fence is verbatim
eink glyphs are constant 4-char width
closed task renders as a single header row, never a standalone task_end
terrarium.CreatureLayoutTest
3
/
3
7ms
no row overlaps more than ~50 percent across realistic counts
low counts keep full size and do not shrink
floor-rest clear anchor stays left of the crayfish claws
terrarium.TerrariumStateTest
10
/
10
299ms
claude processing does not bleed into OpenClaw crayfish on aggregate view
primary Kiro session routes to the canonical vector-mark creature
Kiro IDE sibling does not fall back to an octopus
emitted OpenClaw session shows OpenClaw at rest
gateway error without an emitted session does not spawn a creature
gateway error with a live session surfaces sick OpenClaw
OpenClaw processing routes its own crayfish
reachable gateway without an emitted session hides OpenClaw and workers
stuck gatewayConnected without an emitted session still hides OpenClaw
daemon aggregate keeps OpenClaw crayfish calm while claude works
terrarium.renderer.EinkAnimationTimingTest
3
/
3
5ms
animation frame advance is elapsed-time based and bounded
fish simulation scales movement for partial color frames
color e-ink animation uses video-like cadence
ui.common.ConnectionSetupGuideTest
1
/
1
0ms
disconnected guidance states the host prerequisite and both setup paths
ui.eink.EinkAttentionPanelTest
4
/
4
18ms
non-focused awaiting session hides unavailable live prompt fields
primary awaiting session is surfaced when not represented by siblings
featured attention prefers focused awaiting session
observed AskUserQuestion choices are visible but not actionable
ui.eink.EinkLimitRowTextTest
5
/
5
3ms
no row overruns the card, whatever the label and percentage
the percentage is never truncated, even by an over-long label
the common two-character row keeps the full eight-cell gauge
the gauge still reads as a proportion
a maxed scoped cap keeps its digits and pays for them in gauge cells
ui.eink.SessionDisplayOrderingTest
11
/
11
8ms
naturalLabelCompare orders Agent 2 before Agent 10
compareSessionsForDisplay breaks ties on startedAt ascending (oldest first)
compareSessionsForDisplay maps terminal tab order onto the deck
compareSessionsForDisplay sorts openclaw before claude-code regardless of project
compareSessionsForDisplay tie-breaks on natural id when startedAt is identical
agentTypeRank places openclaw first
sessionWeight accepts the documented bounds and clamps beyond them
compareSessionsForDisplay is stable across re-sorts of any input order
compareSessionsForDisplay orders opposite-sign extreme weights without Int overflow
compareSessionsForDisplay orders by weight ascending before everything else
sessionWeight collapses null to 0 and preserves finite values
ui.monitor.MonitorLayoutScaleTest
8
/
8
10ms
isTablet is true exactly for the classes with room for side rails
eink bands grow with the panel and portrait is never shorter than landscape
side rails cannot overlap at the narrowest width of their size class
rail fractions leave room for the terrarium between them
size class boundaries match the device profile
every size class resolves to an eink scale
every size class resolves to a monitor scale
larger classes never render denser than smaller ones
ui.monitor.OpenClawDisplayLinesTest
4
/
4
4ms
empty when no default tagged
empty catalog yields empty list
keeps only default model when present
empty when default is unavailable
ui.monitor.SubscriptionLineTest
12
/
12
16ms
subscriptionTrailing flags expired for past dates
malformed until renders renewal needed
future ISO8601 with offset renders date suffix
parseUntilInstant accepts ISO8601
bare date in past renders renewal needed
future ISO8601 with fractional seconds renders date suffix
subscriptionTrailing returns date for future
parseUntilInstant accepts blank as null
past until renders renewal needed
bare date string parses as UTC midnight
subscriptionTrailing returns null for blank or null
null until renders name only
ui.monitor.TimelineDayBreakTest
6
/
6
29ms
index 0 anchors an older buffer but stays silent on today
separator lands on the first row of the new day
out-of-range index yields no separator
all-today buffer gets no separator at all
a day older than this year carries the year
same instant reads as a different day across zones
util.DeviceProfileHolderTest
1
/
1
30ms
listeners receive genuine changes but not repeated installation
util.DeviceProfileTest
44
/
44
15ms
onyx mono model is not color eink
real property values are eink signals
hisense eink reader is detected by model
reader with a generic manufacturer is detected from the brand in its model
screen below the floor is unsupported
oneplus x is not an eink device despite its onyx codename
false-like property values are not eink signals
hisense lcd phone is not an eink device
verified hisense eink families retain model detection
generic models named leaf or poke are not eink devices
onyx note air 3 c is color eink
short label prefers a named vendor and otherwise names the form factor
crema stays eink and does not require system fixed rotation
color signal alone establishes eink evidence
onyx boox stays eink
eink override wins over a negative detection
compact screens are supported but flagged as tight
kobo stays eink
auto override is not reported as overridden
size class boundaries
color signal upgrades an otherwise mono verdict
hisense lcd television model numbers do not match short eink families
build characteristics eink is detected
false-like color property values remain rejected
television renders with a no-touch caveat
moaan pantone is color eink and requires system fixed rotation
wire kind keeps the two value vocabulary the swift topology maps
display name does not repeat an embedded brand
eink override keeps a color panel in color
automotive renders with its own caveat
a touchless tablet is limited even without a tv ui mode
panel override round trips through storage
hisense q5 is a reflective lcd not an eink panel
huawei nova is not an eink device
describe names the panel size and evidence
build string only match is flagged as unverified
redmi note is not an eink device
display name degrades gracefully when build fields are empty
unlisted reader with eink system property is detected
watch is unsupported regardless of size
aggregate property check ignores false-like values
lcd override wins over a positive detection
phone sized lcd is handheld and tablet sized lcd is tablet
unlisted reader with vendor eink api is detected
util.SessionGroupingTest
7
/
7
2ms
short siblings stay separate
groups duplicate #N sessions of the same project
delimiter extension fuses
exact match groups
long multi-token stems fuse
normalize strips path and trailing #N suffix
clusters same-stem worktrees and keeps singletons flat
util.TimeFormatUtilsTest
31
/
31
10ms
gaugeBar clamps below 0
formatCount thousands show K
formatBytes kilobytes
formatDurationCompact seconds
codex footnote ended window outranks age
formatDurationCompact exact minutes no seconds
formatDurationCompact sub-second
formatCount int overload works
gaugeBar clamps above 100
formatBytes small values
formatCount small numbers unchanged
gaugeBar 50 percent is half filled
gaugeBar 0 percent is all empty
formatBytes megabytes
formatResetTime returns original on parse failure
codexLimitRows dates an aged reading of a still-live weekly window
gaugeBar custom width
codex footnote fires only past the threshold
codex snapshot threshold matches the TypeScript SSOT
windowLabel maps minutes to compact day-hour-minute labels
formatBytes gigabytes
codexLimitRows maps primary and secondary windows with agent tag
codexLimitRows skips windows with null usedPercent
formatUptime zero returns 0 colon 00
codexLimitRows returns empty when limits null
formatCount millions show M
codex snapshot age ignores missing and malformed stamps
formatDurationCompact minutes
codexLimitRows leaves a legacy producer without capturedAt unchanged
gaugeBar 100 percent is all filled
codex snapshot age label rounds down
▩
ESP32 Firmware Verification
NOT RUN
Robot Framework · physical hardware behavior when an HW-tagged run is supplied · 0 suites · 0 scenarios · 0 tests · 0 boards
No Robot Framework test results available.
▦
Scenario Coverage
User scenario mapping against actual test results
| Score | Scenario | Priority | Unit | Integ. | Platform | E2E |
|---|---|---|---|---|---|---|
| 6/8 |
Session Lifecycle
Start session -> processing -> idle -> stop. Core state machine transitions that drive all UI.
|
critical | 4/4 | 1/2 | 1/2 | — |
| 2/5 |
Permission Flow
Agent requests permission -> user sees options -> approves/denies -> state transitions back.
|
critical | 1/2 | 0/2 | 1/1 | — |
| 7/7 |
Multi-agent Monitoring
Daemon hub orchestrates multiple session bridges, unified dashboard for all agents.
|
high | 3/3 | 4/4 | — | — |
| 7/7 |
Device Connection
mDNS discovery -> auth token validation -> WebSocket/serial state sync with dashboard clients.
|
high | 3/3 | 3/3 | 1/1 | — |
| 4/5 |
Timeline Aggregation
Events from multiple sessions -> daemon relay -> dedup -> unified timeline for all clients.
|
high | 2/2 | 1/1 | 1/2 | — |
| 1/1 |
Voice Recording & Transcription
Audio capture -> whisper transcription -> text delivery to Claude PTY or clipboard.
|
medium | — | 1/1 | — | — |
| 1/3 |
Encoder & Button Interaction
Dial rotate -> option scroll -> push select. Button press -> action trigger. Label rendering.
|
medium | 1/3 | — | — | — |
| 4/5 |
Usage Tracking & Rate Limits
API usage fetch -> rate limit gauge -> sibling relay (429 prevention) -> dashboard display.
|
medium | 0/1 | 2/2 | 2/2 | — |
| 2/2 |
Daemon Singleton Guard
PID check -> port probe -> prevent double daemon. Auto-fallback on port conflict.
|
medium | 1/1 | 1/1 | — | — |
| 1/1 |
Hook Installation & Migration
Install Claude Code hooks -> migrate v2.0 flat format to v2.1 matcher groups -> port injection.
|
medium | 1/1 | — | — | — |
▤
Coverage
v8 provider · 26,257 lines tracked
Lines ≥17%: 55.1%
Functions ≥15%: 54.2%
Branches ≥14%: 49.9%
Statements ≥16%: 54.0%
bridge
Lines
Stmts
Funcs
Branch
11453/21732 lines covered
plugin
Lines
Stmts
Funcs
Branch
1005/1972 lines covered
shared
Lines
Stmts
Funcs
Branch
1562/2066 lines covered
hooks
Lines
Stmts
Funcs
Branch
389/414 lines covered
| File | Stmts | Branch | Funcs | Lines | |
|---|---|---|---|---|---|
| bridge/src/check-deps.ts | 0% | 0% | 0% | 0% | |
| bridge/src/daemon.ts | 0% | 0% | 0% | 0% | |
| bridge/src/diag-analyzer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/event-journal.ts | 0% | 0% | 0% | 0% | |
| bridge/src/index.ts | 0% | 0% | 0% | 0% | |
| bridge/src/log-stream.ts | 0% | 100% | 0% | 0% | |
| bridge/src/pty-ringbuffer.ts | 0% | 0% | 0% | 0% | |
| bridge/src/terminal-status.ts | 0% | 0% | 0% | 0% | |
| bridge/src/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/utility-proxy.ts | 0% | 0% | 0% | 0% | |
| bridge/src/version-check.ts | 0% | 0% | 0% | 0% | |
| bridge/src/voice.ts | 0% | 0% | 0% | 0% | |
| bridge/src/apme/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/idotmatrix/idotmatrix-discover.ts | 0% | 0% | 0% | 0% | |
| bridge/src/timebox/timebox-discover.ts | 0% | 0% | 0% | 0% | |
| bridge/src/tui/screen.ts | 0% | 0% | 0% | 0% | |
| bridge/src/types/picovoice.d.ts | 0% | 0% | 0% | 0% | |
| bridge/src/wake-word.ts | 2% | 0% | 0% | 2% | |
| bridge/src/apme/index.ts | 2% | 0% | 0% | 2% | |
| bridge/src/voice-assistant.ts | 2% | 0% | 0% | 2% | |
| bridge/src/pixoo/pixoo-font.ts | 3% | 0% | 0% | 4% | |
| bridge/src/apme/hw-sampler.ts | 6% | 4% | 20% | 4% | |
| bridge/src/tts.ts | 4% | 0% | 0% | 5% | |
| bridge/src/antigravity-local.ts | 6% | 3% | 18% | 5% | |
| bridge/src/ollama-probe.ts | 5% | 0% | 0% | 5% | |
| bridge/src/adb-reverse.ts | 5% | 6% | 9% | 5% | |
| bridge/src/modules/index.ts | 6% | 0% | 33% | 6% | |
| bridge/src/tui/dashboard.ts | 5% | 11% | 3% | 6% | |
| bridge/src/wifi-config.ts | 6% | 0% | 0% | 7% | |
| bridge/src/timebox/timebox-daemon-sync.ts | 6% | 0% | 0% | 7% | |
| bridge/src/apme/outcome.ts | 8% | 2% | 8% | 8% | |
| bridge/src/modules/idotmatrix-module.ts | 6% | 0% | 0% | 8% | |
| bridge/src/modules/timebox-module.ts | 6% | 0% | 0% | 8% | |
| bridge/src/modules/pixoo-module.ts | 6% | 0% | 0% | 8% | |
| bridge/src/cli.ts | 8% | 4% | 11% | 9% | |
| bridge/src/daemon-server.ts | 9% | 7% | 11% | 9% | |
| bridge/src/pixoo/pixoo-client.ts | 10% | 0% | 0% | 10% | |
| bridge/src/timebox/timebox-settings.ts | 8% | 6% | 8% | 10% | |
| bridge/src/pixoo/pixoo-bridge.ts | 12% | 4% | 4% | 12% | |
| bridge/src/pixoo/pixoo-settings.ts | 11% | 0% | 10% | 12% | |
| bridge/src/idotmatrix/idotmatrix-settings.ts | 18% | 33% | 17% | 15% | |
| bridge/src/idotmatrix/idotmatrix-daemon-sync.ts | 14% | 0% | 0% | 16% | |
| bridge/src/gateway-probe.ts | 12% | 0% | 0% | 16% | |
| bridge/src/review-runner.ts | 16% | 22% | 21% | 17% | |
| bridge/src/modules/serial-module.ts | 17% | 0% | 0% | 18% | |
| bridge/src/modules/adb-module.ts | 17% | 0% | 0% | 19% | |
| bridge/src/session-transcript-timeline.ts | 24% | 16% | 14% | 23% | |
| bridge/src/foundation-models-helper.ts | 29% | 23% | 29% | 28% | |
| bridge/src/display-monitor.ts | 27% | 30% | 28% | 28% | |
| bridge/src/observed-inject.ts | 31% | 18% | 50% | 30% | |
| bridge/src/pixoo/pixoo-sprites.ts | 29% | 15% | 43% | 31% | |
| bridge/src/esp32-serial.ts | 33% | 35% | 24% | 34% | |
| bridge/src/session-activity.ts | 29% | 30% | 42% | 34% | |
| bridge/src/logger.ts | 38% | 21% | 62% | 38% | |
| bridge/src/usage-api.ts | 39% | 35% | 61% | 40% | |
| bridge/src/modules/broadcast-module.ts | 43% | 100% | 25% | 43% | |
| bridge/src/modules/mdns-module.ts | 43% | 100% | 25% | 43% | |
| bridge/src/usage-tracker.ts | 44% | 55% | 23% | 44% | |
| bridge/src/opencode-client.ts | 40% | 38% | 57% | 44% | |
| bridge/src/bridge-core.ts | 43% | 43% | 30% | 45% | |
| bridge/src/ble-sync-spawn.ts | 45% | 33% | 41% | 46% | |
| bridge/src/model-catalog.ts | 40% | 19% | 40% | 47% | |
| bridge/src/pixoo/pixoo-camera.ts | 51% | 37% | 50% | 51% | |
| bridge/src/adapters/opencode-adapter.ts | 49% | 38% | 41% | 53% | |
| bridge/src/windows-service.ts | 53% | 78% | 50% | 54% | |
| bridge/src/hook-server.ts | 54% | 32% | 54% | 56% | |
| bridge/src/codex-auth.ts | 61% | 41% | 40% | 60% | |
| bridge/src/session-focus-relay.ts | 62% | 61% | 56% | 62% | |
| bridge/src/apme/antigravity-transcript.ts | 63% | 74% | 33% | 63% | |
| bridge/src/kiro-session.ts | 62% | 61% | 48% | 65% | |
| bridge/src/ws-server.ts | 65% | 58% | 58% | 66% | |
| bridge/src/passive-observer.ts | 62% | 59% | 66% | 66% | |
| bridge/src/adapters/openclaw.ts | 66% | 56% | 69% | 69% | |
| bridge/src/tui/renderer.ts | 69% | 55% | 92% | 71% | |
| bridge/src/apme/classifier.ts | 65% | 66% | 74% | 71% | |
| bridge/src/adapters/index.ts | 71% | 67% | 100% | 71% | |
| bridge/src/adapters/claude-code.ts | 72% | 40% | 50% | 72% | |
| bridge/src/mdns.ts | 71% | 53% | 83% | 73% | |
| bridge/src/python-ble-runtime.ts | 72% | 64% | 87% | 73% | |
| bridge/src/apme/recommend.ts | 71% | 39% | 67% | 74% | |
| bridge/src/tui/terrarium.ts | 72% | 56% | 78% | 74% | |
| bridge/src/session-registry.ts | 72% | 75% | 59% | 75% | |
| bridge/src/apme/runner.ts | 69% | 59% | 65% | 75% | |
| bridge/src/pty-manager.ts | 76% | 51% | 73% | 75% | |
| bridge/src/tui/ansi.ts | 74% | 49% | 84% | 75% | |
| bridge/src/apme/http.ts | 74% | 66% | 88% | 76% | |
| bridge/src/linux-service.ts | 73% | 100% | 72% | 76% | |
| bridge/src/pixoo/pixoo-renderer.ts | 78% | 66% | 82% | 79% | |
| bridge/src/codex-rate-limits-live.ts | 80% | 74% | 67% | 80% | |
| bridge/src/mlx-probe.ts | 76% | 38% | 25% | 81% | |
| bridge/src/adapters/monitor.ts | 81% | 60% | 87% | 81% | |
| bridge/src/pixoo/pixoo-discover.ts | 74% | 63% | 75% | 82% | |
| bridge/src/adapters/pty-adapter.ts | 82% | 81% | 65% | 82% | |
| bridge/src/kiro-diagnostics.ts | 85% | 64% | 87% | 84% | |
| bridge/src/session-timeline-relay.ts | 80% | 62% | 71% | 84% | |
| bridge/src/daemon-takeover.ts | 84% | 77% | 100% | 84% | |
| bridge/src/apme/store.ts | 72% | 73% | 88% | 84% | |
| bridge/src/state-machine.ts | 84% | 68% | 87% | 85% | |
| bridge/src/apme/adapters/codex-hook.ts | 79% | 60% | 100% | 85% | |
| bridge/src/apme/claude-transcript-reader.ts | 82% | 77% | 86% | 85% | |
| bridge/src/kiro-timeline-feed.ts | 84% | 65% | 86% | 86% | |
| bridge/src/broadcast.ts | 79% | 56% | 56% | 87% | |
| bridge/src/device-photo.ts | 88% | 87% | 87% | 88% | |
| bridge/src/apme/collector.ts | 85% | 73% | 89% | 88% | |
| bridge/src/glance-frame.ts | 89% | 80% | 84% | 88% | |
| bridge/src/apme/sample-to-timeline.ts | 84% | 67% | 100% | 90% | |
| bridge/src/pocket-autonomy.ts | 86% | 66% | 89% | 91% | |
| bridge/src/daemon-ws-client.ts | 85% | 72% | 81% | 91% | |
| bridge/src/usage-event.ts | 92% | 86% | 100% | 91% | |
| bridge/src/device-voice.ts | 89% | 74% | 83% | 92% | |
| bridge/src/session-aggregator.ts | 91% | 75% | 100% | 92% | |
| bridge/src/adapters/codex-cli.ts | 92% | 100% | 88% | 92% | |
| bridge/src/device-voice-reply.ts | 85% | 76% | 65% | 92% | |
| bridge/src/network-posture.ts | 93% | 88% | 100% | 92% | |
| bridge/src/mdns-discover.ts | 90% | 74% | 100% | 93% | |
| bridge/src/codex-rollout-response.ts | 89% | 86% | 89% | 93% | |
| bridge/src/codex-output-parser.ts | 90% | 84% | 95% | 94% | |
| bridge/src/output-parser.ts | 90% | 84% | 100% | 94% | |
| bridge/src/apme/adapters/timeline.ts | 95% | 76% | 100% | 94% | |
| bridge/src/opencode-steering.ts | 95% | 87% | 100% | 94% | |
| bridge/src/peripheral-mapping.ts | 96% | 95% | 100% | 94% | |
| bridge/src/pixoo/micro-glyphs.ts | 92% | 86% | 100% | 94% | |
| bridge/src/auth.ts | 92% | 88% | 100% | 95% | |
| bridge/src/fallback-task-timeline.ts | 95% | 82% | 83% | 95% | |
| bridge/src/apme/classify-turn.ts | 91% | 80% | 100% | 95% | |
| bridge/src/apme/adapters/codex-turn-manager.ts | 90% | 79% | 100% | 95% | |
| bridge/src/kiro-transcript-timeline.ts | 88% | 80% | 100% | 95% | |
| bridge/src/claude-turn-watchdog.ts | 93% | 91% | 88% | 95% | |
| bridge/src/utils/project-name.ts | 93% | 85% | 100% | 96% | |
| bridge/src/codex-otel.ts | 89% | 84% | 100% | 96% | |
| bridge/src/subagent-timeline.ts | 95% | 82% | 100% | 96% | |
| bridge/src/ask-gate.ts | 97% | 97% | 100% | 96% | |
| bridge/src/pairing-window.ts | 95% | 89% | 88% | 96% | |
| bridge/src/observed-steering.ts | 93% | 87% | 96% | 97% | |
| bridge/src/codex-rate-limits.ts | 93% | 84% | 100% | 97% | |
| bridge/src/calendar.ts | 95% | 84% | 96% | 97% | |
| bridge/src/session-push-channel.ts | 97% | 82% | 50% | 97% | |
| bridge/src/timeline-summarizer.ts | 92% | 54% | 75% | 97% | |
| bridge/src/observed-turn-watchdogs.ts | 96% | 75% | 100% | 97% | |
| bridge/src/awaiting-overlay.ts | 92% | 85% | 100% | 98% | |
| bridge/src/permission-resolver.ts | 94% | 70% | 91% | 98% | |
| bridge/src/apme/adapters/opencode-hook.ts | 98% | 84% | 100% | 98% | |
| bridge/src/hook-codex-sessions.ts | 93% | 87% | 83% | 98% | |
| bridge/src/apme/adapters/openclaw-hook.ts | 96% | 82% | 100% | 98% | |
| bridge/src/timeline-store.ts | 96% | 85% | 100% | 99% | |
| bridge/src/weather.ts | 95% | 86% | 92% | 99% | |
| bridge/src/card-feed.ts | 92% | 83% | 100% | 99% | |
| bridge/src/ble-sync-status.ts | 100% | 91% | 100% | 100% | |
| bridge/src/card-modules.ts | 100% | 90% | 100% | 100% | |
| bridge/src/claude-interrupt-marker.ts | 95% | 92% | 100% | 100% | |
| bridge/src/claude-permission-rules.ts | 93% | 80% | 100% | 100% | |
| bridge/src/codex-hook-silence.ts | 94% | 79% | 100% | 100% | |
| bridge/src/daemon-port.ts | 100% | 88% | 100% | 100% | |
| bridge/src/daemon-settings.ts | 95% | 75% | 100% | 100% | |
| bridge/src/display-dim.ts | 95% | 69% | 100% | 100% | |
| bridge/src/http-auth-gate.ts | 100% | 93% | 100% | 100% | |
| bridge/src/openclaw-session.ts | 100% | 100% | 100% | 100% | |
| bridge/src/remote-sessions.ts | 100% | 100% | 100% | 100% | |
| bridge/src/session-id-resolve.ts | 100% | 93% | 100% | 100% | |
| bridge/src/apme/dashboard-html.ts | 100% | 100% | 100% | 100% | |
| bridge/src/apme/graph.ts | 95% | 83% | 100% | 100% | |
| bridge/src/apme/judge-detect.ts | 98% | 85% | 100% | 100% | |
| bridge/src/apme/pareto.ts | 100% | 81% | 100% | 100% | |
| bridge/src/apme/settings.ts | 97% | 86% | 100% | 100% | |
| bridge/src/apme/adapters/claude-hook.ts | 100% | 77% | 100% | 100% | |
| bridge/src/apme/scorers/index.ts | 98% | 73% | 100% | 100% | |
| bridge/src/modules/types.ts | 100% | 100% | 100% | 100% | |
| bridge/src/pixoo/official-dot-glyphs.generated.ts | 100% | 100% | 100% | 100% | |
| bridge/src/tui/gauge.ts | 96% | 93% | 100% | 100% | |
| bridge/src/utils/redact-secrets.ts | 100% | 50% | 100% | 100% | |
| hooks/src/install.ts | 89% | 87% | 93% | 91% | |
| hooks/src/opencode-install.ts | 93% | 82% | 100% | 93% | |
| hooks/src/codex-install.ts | 91% | 82% | 96% | 93% | |
| hooks/src/codex-mini-toml.ts | 98% | 94% | 100% | 100% | |
| plugin-ulanzi/src/reconnect-supervisor.ts | 85% | 68% | 75% | 89% | |
| plugin/src/agent-link.ts | 0% | 0% | 0% | 0% | |
| plugin/src/display-dim.ts | 0% | 0% | 0% | 0% | |
| plugin/src/encoder-registry.ts | 0% | 100% | 0% | 0% | |
| plugin/src/plugin.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/iterm-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/launcher-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/option-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/session-slot-button.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/usage-limit-button.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/utility-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/agent-logos.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/button-renderer.ts | 0% | 100% | 0% | 0% | |
| plugin/src/renderers/session-slot-renderer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/system/darwin.ts | 0% | 0% | 0% | 0% | |
| plugin/src/system/types.ts | 0% | 0% | 0% | 0% | |
| plugin/src/bridge-client.ts | 55% | 35% | 67% | 59% | |
| plugin/src/system/index.ts | 65% | 100% | 50% | 62% | |
| plugin/src/log.ts | 59% | 50% | 33% | 67% | |
| plugin/src/system/win32.ts | 63% | 80% | 62% | 67% | |
| plugin/src/renderers/display-tile.ts | 77% | 53% | 100% | 80% | |
| plugin/src/session-slot-manager.ts | 85% | 78% | 89% | 86% | |
| plugin/src/utility-modes/usage.ts | 83% | 81% | 87% | 87% | |
| plugin/src/focused-detail-state.ts | 91% | 76% | 86% | 90% | |
| plugin/src/connection-manager.ts | 92% | 88% | 82% | 95% | |
| plugin/src/system/win32-volume.ts | 94% | 81% | 92% | 97% | |
| plugin/src/renderers/usage-gauge.ts | 96% | 84% | 94% | 98% | |
| plugin/src/device-profile.ts | 100% | 100% | 100% | 100% | |
| plugin/src/launch-targets.ts | 100% | 93% | 100% | 100% | |
| plugin/src/renderers/launcher-renderer.ts | 100% | 50% | 100% | 100% | |
| plugin/src/renderers/usage-dial-renderer.ts | 90% | 47% | 100% | 100% | |
| plugin/src/renderers/utility-renderer.ts | 100% | 75% | 100% | 100% | |
| shared/src/adapter.ts | 0% | 100% | 100% | 0% | |
| shared/src/apme-graph.ts | 0% | 100% | 0% | 0% | |
| shared/src/command-builders.ts | 0% | 100% | 0% | 0% | |
| shared/src/eval-schema.ts | 0% | 0% | 0% | 0% | |
| shared/src/gateway-protocol.ts | 0% | 100% | 100% | 0% | |
| shared/src/index.ts | 0% | 0% | 0% | 0% | |
| shared/src/net-utils.ts | 0% | 0% | 0% | 0% | |
| shared/src/sample.ts | 0% | 0% | 0% | 0% | |
| shared/src/telemetry-envelope.ts | 0% | 0% | 0% | 0% | |
| shared/src/timeline-label.ts | 0% | 0% | 0% | 0% | |
| shared/src/voice-paths.ts | 0% | 0% | 0% | 0% | |
| shared/src/svg-renderers/index.ts | 0% | 0% | 0% | 0% | |
| shared/src/state-colors.ts | 38% | 50% | 67% | 42% | |
| shared/src/svg-renderers/agent-logos.ts | 50% | 39% | 58% | 52% | |
| shared/src/svg-renderers/text-utils.ts | 55% | 47% | 86% | 60% | |
| shared/src/format-utils.ts | 64% | 66% | 71% | 64% | |
| shared/src/svg-renderers/session-slot-renderer.ts | 64% | 59% | 74% | 64% | |
| shared/src/d200h-layout.ts | 75% | 74% | 84% | 75% | |
| shared/src/timeline-summarizer.ts | 73% | 63% | 57% | 76% | |
| shared/src/timeline.ts | 79% | 74% | 100% | 86% | |
| shared/src/timeline-icons.ts | 86% | 79% | 80% | 89% | |
| shared/src/subagent-activity.ts | 90% | 77% | 86% | 93% | |
| shared/src/session-utils.ts | 95% | 86% | 100% | 96% | |
| shared/src/pricing.ts | 91% | 77% | 88% | 96% | |
| shared/src/creature-layout.ts | 94% | 89% | 86% | 98% | |
| shared/src/connection-status.ts | 100% | 100% | 100% | 100% | |
| shared/src/design-tokens.ts | 100% | 100% | 100% | 100% | |
| shared/src/idotmatrix-identity.ts | 100% | 94% | 100% | 100% | |
| shared/src/llm-settings.ts | 100% | 98% | 100% | 100% | |
| shared/src/mdns-identity.ts | 100% | 100% | 100% | 100% | |
| shared/src/model-provider.ts | 100% | 92% | 100% | 100% | |
| shared/src/openclaw-approval.ts | 97% | 93% | 100% | 100% | |
| shared/src/pairing-code.ts | 98% | 96% | 100% | 100% | |
| shared/src/protocol.ts | 100% | 100% | 100% | 100% | |
| shared/src/states.ts | 100% | 100% | 100% | 100% | |
| shared/src/terrarium-rules.ts | 100% | 100% | 100% | 100% | |
| shared/src/timeline-markdown.ts | 99% | 92% | 100% | 100% | |
| shared/src/timeline-task-display.ts | 100% | 97% | 100% | 100% | |
| shared/src/voice-ptt.ts | 100% | 100% | 100% | 100% | |
| shared/src/voice-reply-digest.ts | 97% | 90% | 100% | 100% |