Status
FAIL
1803 executed tests
Passed
1799
99.8% pass rate
Failed
4
Duration
19.4s
13.9s vitest + 5.6s android
TS Line Coverage
40.6%
8,334/20,540 TypeScript lines
Total Tests
1803
Pass Rate
99.8%
Line Coverage
40.6%
◈
Terminal Output Parsing
PASS
Claude Code와 Codex CLI의 터미널 출력을 정확히 해석하는가?
257 passed
0 failed
3 files
273ms
output-parser.test.ts
214
/
214
189ms
OutputParser
214 / 214
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 › 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 › 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)
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
28
/
28
44ms
CodexOutputParser
28 / 28
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
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
41ms
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
에이전트의 상태 전이와 타입별 명령 라우팅이 정확한가?
178 passed
0 failed
3 files
186ms
state-machine.test.ts
60
/
60
61ms
StateMachine
60 / 60
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
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 survives the 5min PROCESSING window but recovers after the 10min backstop
stuck timeout › AWAITING_OPTION recovers after the 10min backstop
stuck timeout › AWAITING_DIFF recovers after the 10min backstop
stuck timeout › AWAITING backstop resets when the user responds (no false recovery)
stuck timeout › AWAITING backstop re-arms on PTY activity (silence-based, not a hard cap from entry)
stuck timeout › timer resets on state change before timeout
stuck timeout › no timeout in IDLE state
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 is a snapshot-emit no-op for state
codex_* hook events › full codex lifecycle preserves state transitions
adapter.test.ts
93
/
93
95ms
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
2 / 2
emits SessionStart and connected on start
feeds PTY data to output parser and emits activity
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
5 / 5
emits SessionStart and connected on start
uses claude as default command
feeds PTY data to output parser and emits activity
emits SessionEnd and disconnected on PTY exit
registers rawData callback without error
protocol-contract.test.ts
25
/
25
30ms
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
이벤트 타임라인의 저장, 중복 제거, 세션 간 릴레이가 올바른가?
111 passed
0 failed
3 files
1.3s
timeline.test.ts
64
/
64
34ms
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
9 / 9
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
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
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
11 / 11
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
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
timeline-integration.test.ts
42
/
42
39ms
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 (PTY/Stop 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)
Stop hook + PTY fallback 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
데몬 싱글톤, 세션 레지스트리, 사용량 릴레이가 안정적인가?
72 passed
0 failed
4 files
2.5s
daemon-lifecycle.test.ts
19
/
19
55ms
daemon.json lifecycle
6 / 6
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
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
21
/
21
1.3s
Session Registry Logic
21 / 21
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
usage-relay.test.ts
12
/
12
635ms
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
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
20
/
20
514ms
BridgeCore Orchestration
20 / 20
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
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
전체 파이프라인이 실제 서버 환경에서 동작하는가?
33 passed
0 failed
2 files
1.1s
server-integration.test.ts
17
/
17
771ms
Server Integration
17 / 17
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
tier3-integration.test.ts
16
/
16
333ms
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
플러그인의 연결 관리, 옵션 레이아웃, 렌더링이 정확한가?
135 passed
0 failed
5 files
1.8s
connection-manager.test.ts
16
/
16
18ms
ConnectionManager
16 / 16
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
isGatewayAvailable() › returns false by default
isGatewayAvailable() › returns true when bridge reports gateway available
isGatewayAvailable() › returns false when bridge reports gateway unavailable
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
option-scenario.test.ts
9
/
9
7ms
option-renderer panels
3 / 3
E2 Focus panel renders with adaptive font
E3 List panel renders 4 visible rows with 14px font
E4 Detail panel shows word-wrapped label (12px, left-aligned)
colorForOption — "don't ask again" / "allow all sessions"
6 / 6
returns blue for "Yes, and don't ask again for: tail:*"
returns blue for "Yes, and don’t ask again" (smart quote)
returns blue for "Yes, allow all sessions in project"
returns green for plain "Yes" (shortcut y)
returns green for "Apply" (shortcut a, but no "don't ask" pattern)
returns blue for "Always allow"
renderer-snapshots.test.ts
85
/
85
78ms
voice-renderer snapshots
12 / 12
renderVoiceReady
renderVoiceRecording frame 0
renderVoiceRecording frame 30 (>1min)
renderVoiceTranscribing frame 0
renderVoiceTranscribing frame 10 (different dot phase)
renderVoiceError with message
renderVoiceError default
renderVoiceDisabled
renderVoiceAssistantListening frame 0
renderVoiceAssistantProcessing frame 5
renderVoiceAssistantSpeaking frame 0
renderWideVoiceText returns correct panel count
utility-renderer snapshots
4 / 4
renderSetupUtility
renderUtilityGeneric with icon
renderUtilityGeneric text-only (no icon)
renderUtilityMedia
usage-dial-renderer snapshots
7 / 7
renderUsageOverview
renderUsageDetail 5h
renderUsageDetail 7d
renderUsageSession
renderUsageExtra enabled
renderUsageExtra disabled
renderUsageDisconnected
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
response-renderer snapshots
7 / 7
renderResponseIdle
renderResponseProcessing
renderResponseDisconnected
renderResponseDisabled
renderResponseSuggestion
renderResponseInteractive
renderSetupPrompt
option-renderer snapshots
7 / 7
renderContextPanel permission
renderContextPanel diff
renderFocusPanel with recommended option
renderListPanel 3 options
renderListPanel 6 options (scroll indicator)
renderDetailPanel
renderWideOptionList returns correct panel count
button-renderer snapshots
7 / 7
basic text button
button with subtitle
disabled button
loading button
long text that needs abbreviation
svgToDataUrl returns data URI
labelNeedsHaiku detects long labels
session-slot-renderer snapshots
9 / 9
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
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
timeline-renderer snapshots
5 / 5
empty timeline
single entry
multiple entries fisheye
detail mode
with session status
qr-renderer snapshots
3 / 3
extractUrlLabel extracts host:port
qrPathData deterministic
renderQrButtonSvg
agent-logos snapshots
4 / 4
claude-code watermark
openclaw watermark
antigravity icon uses the full-color mark
CLAUDE_LOGO_PATH is defined
text-utils-and-labels.test.ts
19
/
19
11ms
text-utils: CJK width measurement
4 / 4
Latin text is ~0.55em per char
Korean text is 1em per char (double-width)
mixed text measures correctly
isWide detects Hangul, CJK, fullwidth
text-utils: wrapTextByWidth
3 / 3
short text returns single line
long Latin text wraps to multiple lines
Korean text wraps at correct pixel width
button-renderer: abbreviation
10 / 10
short label renders without abbreviation indicator
"Yes, I trust this folder" fits with wrapping (no abbreviation needed)
"Yes, allow and don't ask again" wraps into 3 lines
very long label triggers abbreviation with ~ indicator
short permission labels like "No" render unchanged
labelNeedsHaiku returns false for short labels
labelNeedsHaiku returns false when heuristic abbreviation fits
labelNeedsHaiku returns true for very long unknown labels
labelNeedsHaiku returns false when Haiku cache has result
BUTTON_MAX_CHARS is reasonable
button-renderer: CJK labels
2 / 2
Korean label does not overflow (produces valid SVG)
mixed CJK/Latin label renders
▤
TUI Dashboard
PASS
터미널 대시보드의 상태 렌더링과 테라리움 애니메이션이 올바른가?
51 passed
0 failed
3 files
70ms
tui-dashboard.test.ts
9
/
9
22ms
TUI dashboard models
9 / 9
stores modelCatalog from state_update
renders OAuth catalog and Ollama models in wide layout
renders disconnected OAuth state
renders downstream module health compactly
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
17ms
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
13
/
13
31ms
TUI terrarium snapshots
13 / 13
initTerrarium creates context with expected structure
setOctopi configures octopus instances
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
ESP32와의 시리얼 바이트스트림이 정확히 프레이밍되는가?
40 passed
0 failed
1 files
34ms
esp32-serial-node.test.ts
40
/
40
34ms
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)
5 / 5
parses device_info message and updates deviceInfo
skips debug lines (non-JSON)
recovers from malformed JSON
ignores JSON without type field
does not mark deviceInfo present for non-identifying messages
serial I/O error classification
2 / 2
treats nonblocking backpressure as retryable
does not retry terminal serial errors
prepareForSerial (source)
10 / 10
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
passes through other events unchanged
WiFi provision protocol
1 / 1
provision message has all required fields
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
▦
Display Rendering
PASS
외부 디스플레이에 전송할 이미지 데이터가 올바른가?
19 passed
0 failed
1 files
89ms
pixoo-sprites.test.ts
19
/
19
89ms
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
Claude Code hook의 설치, 제거, 마이그레이션이 안전한가?
24 passed
0 failed
1 files
15ms
install.test.ts
24
/
24
15ms
Hook Installer
24 / 24
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) › 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
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
○
Other Tests
Uncategorized test files
681 passed
0 failed
62 files
antigravity-transcript.test.ts
3
/
3
5ms
antigravity transcript parsing
3 / 3
extracts goal, model (from settings change), state and tool task
reports processing when any step is RUNNING; later model change wins
matches the agy CLI binary as an Antigravity process
apme-category-e2e.test.ts
3
/
3
59ms
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
69ms
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
13
/
13
191ms
ApmeCollector
13 / 13
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
apme-http.test.ts
13
/
13
525ms
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-probe.test.ts
20
/
20
103ms
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 (stub backend)
2 / 2
returns unavailable when ANTHROPIC_API_KEY is not set
returns unavailable even when ANTHROPIC_API_KEY is set (callApi is a stub)
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-pareto.test.ts
4
/
4
32ms
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
34
/
34
609ms
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
1 / 1
includes rubric prompt + task + context fields
apme-sample-timeline.test.ts
8
/
8
6ms
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
98ms
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
15
/
15
18ms
loadApmeConfig — defaults + merge behaviour
12 / 12
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
downgrades judge.backend="api" to "mlx" on the Node bridge (callApi is a stub)
also wipes endpoint/model when downgrading backend="api" → "mlx"
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-task-boundary.test.ts
13
/
13
242ms
ApmeCollector task boundaries
11 / 11
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 fires with sessionId + agentType + projectName + taskIndex
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
533ms
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-telemetry-envelope.test.ts
40
/
40
360ms
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
claudePtyParserEventToSpans
4 / 4
maps tool_start to a tool_call span
maps tool_end to a tool_result span
emits a raw_step for spinner_start/idle/spinner_stop
drops unknown parser events
claudePtyResponseToSpan
3 / 3
returns a turn_response span for non-trivial text
marks fallback_to_last_closed when requested
returns null for empty / single-character text (filters silence)
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
awaiting-overlay.test.ts
18
/
18
16ms
awaiting-overlay
18 / 18
set then get returns the trimmed question
carries an optional requestId (actionable PreToolUse gate)
returns undefined for an unknown session
expires after the TTL (5 min)
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 / auto / 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
ble-sync-squelch.test.ts
5
/
5
10ms
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
bridge-core-sessions.test.ts
2
/
2
71ms
BridgeCore sessions_list
2 / 2
broadcastSessionsList enriches sessions before broadcast
sendInitialState sends enriched sessions_list to the connecting client
claude-transcript-reader.test.ts
7
/
7
12ms
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
cli.test.ts
1
/
1
6ms
agentdeck CLI parser
1 / 1
reports a misspelled top-level command as unknown and suggests the closest command
codex-rate-limits.test.ts
9
/
9
9ms
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
codex-turn-manager.test.ts
9
/
9
106ms
CodexTurnManager (hook-primary path)
6 / 6
happy path: UPS → tool_start → tool_end → stop emits one chat
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
hook freshness window suppresses PTY parser idle close
long-bash: hook tool_start + tool_end keep the same turn open
CodexTurnManager (PTY-only fallback when hooks absent)
3 / 3
spinner_start opens turn, prompt-source idle closes after deferral
timeout-source idle without prior tool_action does not latch
tool_action then timeout-idle latches; spinner_start closes prev + opens new
d200h-button-map.test.ts
7
/
7
12ms
D200H buildButtonCommandMap
7 / 7
single-session AWAITING: mode/focus/options/usage/interrupt at expected keys
multi-session AWAITING: sessions focus by real id, options on row 1
observed session (requestId, no options): no fabricated Allow/Deny keys
awaiting with no options and no requestId: no fake Yes/No/Always keys
options are inert unless AWAITING
no real session: hero focus is inert (no synthetic id dispatched)
DISCONNECTED frame has no actionable keys
device-discovery.test.ts
2
/
2
5ms
Pixoo discovery — config-reply classification
2 / 2
accepts a real GetAllConf reply
rejects a non-Pixoo HTTP/JSON response
display-dim.test.ts
3
/
3
6ms
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
fallback-task-timeline.test.ts
4
/
4
9ms
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
gateway-parity-fixtures.test.ts
33
/
33
10ms
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
5ms
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
openclaw-hook.test.ts
8
/
8
10ms
openclaw-hook → telemetry spans
8 / 8
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 nothing — the agent may retry, idle timer keeps running
idle-gap task_boundary span carries boundary_signal=idle_gap
all emitted spans propagate traceId for run correlation
openclaw-session.test.ts
5
/
5
5ms
injectOpenClawSession
5 / 5
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
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
70ms
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
6
/
6
14ms
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
opencode-client.test.ts
13
/
13
17ms
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)
passive-observer.test.ts
9
/
9
10ms
passive-observer parsers
9 / 9
parses ps output without depending on fixed command columns
summarizes Claude transcripts and redacts tool secrets
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
recognizes standalone Antigravity processes for CLI daemon passive discovery
permission-resolver.test.ts
12
/
12
20ms
permission-resolver
12 / 12
register → resolve(allow) ends the held response with allow
resolve(deny) ends with deny
times out to "ask" after timeoutMs
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 resolves entries older than maxAge to ask
re-registering a duplicate requestId resolves the stale entry to ask 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 "ask" on timeout
drainAllPending resolves every held response to ask
project-name.test.ts
18
/
18
101ms
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
session-aggregator.test.ts
8
/
8
23ms
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
8ms
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
timebox-ble.test.ts
10
/
10
12ms
Timebox device identity
1 / 1
uses the BLE address as the device id
micro layout (Timebox 11×11)
9 / 9
returns a size²·3 RGB buffer
draws a bright dominant creature for a processing session
shows only the status field (no bright creature) when no sessions exist
tints the field red on critical usage (≥90%)
draws the Claude robot as a block mark with dark cutout eyes
draws the Codex cloud with a visible prompt mark
draws OpenCode as one tall hollow ring
draws OpenClaw with side claws and teal eyes
draws the Antigravity rainbow peak with transparent center hollow
timeline-orphan-reaper.test.ts
2
/
2
5ms
BridgeTimelineStore.reapOrphanTaskStarts
2 / 2
synthesizes task_end for orphaned task_start rows only
lets a real task_end merge over the synthetic by taskId
timeline-projection-cutover.test.ts
4
/
4
6ms
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-task-retention.test.ts
4
/
4
359ms
BridgeTimelineStore task-row retention
4 / 4
keeps an in-flight task_start alive through FIFO overflow
evicts closed task pairs once the task cap is exceeded, oldest first
getHistoryForSession returns task rows beyond the per-session limit
loadPersistedEntries keeps task rows past the generic trim
tui-hud-entries.test.ts
16
/
16
24ms
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-event.test.ts
7
/
7
7ms
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 Codex window normalization
3 / 3
marks an expired window stale and drops its resetsAt (no misleading "now")
leaves a live window untouched
treats each window independently (5h expired, 7d live)
windows-service.test.ts
6
/
6
7ms
xmlEscape
1 / 1
escapes XML metacharacters
buildScheduledTaskXml
5 / 5
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
XML-escapes special characters in the user id and paths
places both UserId fields (trigger + principal) with the same user
codex-install.test.ts
32
/
32
35ms
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: quoted
3 / 3
escapes backslash and double quote
escapes newline and tab
passes simple ASCII
codex-install: managedBlockBody
3 / 3
roundtrip preserves user TOML byte-for-byte (load-bearing)
matches Codex schema (lifecycle hooks + endpoints + notify dummy)
omits conflicting optional channels when asked
codex-install: install / uninstall (file I/O)
11 / 11
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
skips when user already has [hooks] table outside fence
omits notify when user has top-level notify
omits OTel when user has [otel] table
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
reconnect-supervisor.test.ts
8
/
8
5ms
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
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
encoder-offline-policy.test.ts
3
/
3
5ms
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
session-slot-button.test.ts
12
/
12
6ms
computeCenterSlot
6 / 6
SD+ 4x2 → bottom-center
SD MK2 5x3 → true geometric center
SD XL 8x4 → middle row, mid column
SD Mini 3x2 → bottom-center
single key device → slot 0
clamps degenerate zero rows/cols to slot 0
computeCenterCluster
6 / 6
SD+ 4x2 → 2x2 cluster on geometric center (slots 1,2,5,6)
SD XL 8x4 → 2x2 cluster on geometric center (slots 11,12,19,20)
SD MK2 5x3 → single full hero on slot 7
SD Mini 3x2 (odd cols) → single full hero on slot 4
single-key device → single full hero on slot 0
degenerate zero dimensions → single hero on slot 0
session-slot-manager.test.ts
28
/
28
48ms
SessionSlotManager detail layout
12 / 12
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
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
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
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
12 / 12
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)
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
usage-encoder.test.ts
8
/
8
6ms
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)
d200h-layout.test.ts
12
/
12
16ms
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
format-utils.test.ts
24
/
24
10ms
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
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)
llm-settings.test.ts
10
/
10
12ms
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
session-deck-usage.test.ts
13
/
13
20ms
buildSessionDeck list-view usage tiles
13 / 13
pins 5H/7D to the block above the wide button and wires them to query_usage
does not reserve usage slots when showUsage is off (regression: full grid)
draws "—" when usage is unknown
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
appends Codex 5H/7D below Claude when codexRateLimits is present (each datum)
renders only the Codex window whose datum exists
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 block is not placed
never starves the only key on a 1-key deck (usage yields to the session)
session-utils.test.ts
16
/
16
18ms
OpenClaw visibility SSOT
2 / 2
isOpenClawSessionActive is true only when gatewayConnected
hasOpenClawSession detects an emitted openclaw session
agentTypeRank
1 / 1
ranks openclaw first, then claude-code, codex-cli, codex-app, opencode, antigravity, others
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
foldCodexSessionsForDisplay
1 / 1
folds Codex CLI and Codex App separately even with the same project
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
svg-renderers/model-alias.test.ts
7
/
7
4ms
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
text-sanitize.test.ts
5
/
5
7ms
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
8ms
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-hierarchy.test.ts
41
/
41
14ms
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
5 / 5
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
isRotatingEntry
5 / 5
chat_start always rotates (icon-key running)
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)
▣
Android
PASS
JUnit + Robolectric · 14 files · 195 tests
data.DashboardOrientationTest
3
/
3
3ms
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.BridgeDiscoveryTest
2
/
2
12ms
ws url uses primary host and preserves pairing token
fallback url is absent without a fallback host
net.ProtocolTest
34
/
34
5.2s
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
state.SessionMetricsTest
8
/
8
7ms
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
9
/
9
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
same timestamp summaries stay separate by agent and project
state.TimelineStoreTest
50
/
50
42ms
addEntry drops openclaw placeholder with status-only detail
timelineDisplayGroups hides model_call after model_response in same run
addEntry allows same type+summary after 5s
addEntries filters otel noise from bulk replay
clear empties the store
upsertEntry updates existing entry within 1s tolerance
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
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
groupConsecutive does not merge tool_request across sessions
groupConsecutive groups same summary within 60s
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
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
upsertEntry preserves timeline attribution
groupConsecutive splits different types
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
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
updateLastOfType no-op if type not found
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
29
/
29
23ms
OpenClaw NO_REPLY polling responses are hidden from display projection
mismatched taskId on task_end does not close it
OpenClaw automated polling chat starts are hidden from display projection
stripMarkdownInline drops markers including table syntax
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
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 without matching task_end is in flight
idle_gap task boundary is hidden from display projection
chat_start always rotates via icon-key running
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
iconKey resolves to Task for task entries
eval_result and task_end never rotate
manual task boundary is visible in display projection
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
terrarium.TerrariumStateTest
8
/
8
225ms
claude processing does not bleed into OpenClaw crayfish on aggregate view
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
4ms
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.eink.EinkAttentionPanelTest
3
/
3
14ms
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
ui.eink.SessionDisplayOrderingTest
6
/
6
3ms
naturalLabelCompare orders Agent 2 before Agent 10
compareSessionsForDisplay breaks ties on startedAt ascending (oldest first)
compareSessionsForDisplay sorts openclaw before claude-code regardless of project
compareSessionsForDisplay tie-breaks on natural id when startedAt is identical
agentTypeRank places openclaw first
compareSessionsForDisplay is stable across re-sorts of any input order
ui.monitor.OpenClawDisplayLinesTest
4
/
4
3ms
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
10ms
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
util.TimeFormatUtilsTest
24
/
24
19ms
gaugeBar clamps below 0
formatCount thousands show K
formatBytes kilobytes
formatDurationCompact seconds
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
gaugeBar custom width
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
formatDurationCompact minutes
gaugeBar 100 percent is all filled
▩
Robot Framework
FAIL
ESP32 Hardware Tests · 1 suites · 4 scenarios · 7 tests · 4 boards
| Board | Build | Flash+Boot | Boot Time | FW Size | Boot Heap | Latency |
|---|---|---|---|---|---|---|
| Round | 0.6s | — | — | — | — | — |
| IPS 3.5" | 0.6s | — | — | — | — | — |
| TC001 | 0.6s | — | — | — | — | — |
| 86Box | 115.8s | — | — | — | — | — |
01_build.robot
3
/
7
Build And Verify
4 boards
1 / 4
Given the "${board}" firmware is built
Then the firmware binary should exist for "${board}"
And the firmware size should be sane for "${board}"
And the partitions binary should exist for "${board}"
✓ 86Box✗ IPS 3.5"✗ Round✗ TC001
Box 86 Build And Verify
IPS 3.5 Build And Verify
PlatformIO build failed for ips_35:
UnknownEnvNamesError: Unknown environment names 'ips_35'. Valid names are 'rgb48, amoled, ips10, boot_test_qspi, display_test, led8x32, qspi_test, esp32_c6_147, ips35, axs_test, inkdeck, box_86, ttgo': 1 != 0
Round AMOLED Build And Verify
PlatformIO build failed for round_amoled:
UnknownEnvNamesError: Unknown environment names 'round_amoled'. Valid names are 'box_86, inkdeck, ips35, display_test, ttgo, esp32_c6_147, led8x32, boot_test_qspi, qspi_test, rgb48, axs_test, amoled, ips10': 1 != 0
Ulanzi TC001 Build And Verify
PlatformIO build failed for ulanzi_tc001:
UnknownEnvNamesError: Unknown environment names 'ulanzi_tc001'. Valid names are 'boot_test_qspi, qspi_test, amoled, led8x32, display_test, ttgo, esp32_c6_147, rgb48, box_86, ips35, axs_test, inkdeck, ips10': 1 != 0
Boot Test Environment Builds Successfully
PlatformIO build failed for boot_test:
UnknownEnvNamesError: Unknown environment names 'boot_test'. Valid names are 'ips10, boot_test_qspi, qspi_test, esp32_c6_147, amoled, rgb48, ips35, display_test, led8x32, box_86, ttgo, inkdeck, axs_test': 1 != 0
Source Files Are Present
PlatformIO Configuration Parses Cleanly
▦
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 | — |
| 3/5 |
Permission Flow
Agent requests permission -> user sees options -> approves/denies -> state transitions back.
|
critical | 2/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 | — | — |
| 2/3 |
Encoder & Button Interaction
Dial rotate -> option scroll -> push select. Button press -> action trigger. Label rendering.
|
medium | 2/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 · 20,540 lines tracked
Lines ≥17%: 40.6%
Functions ≥15%: 37.4%
Branches ≥14%: 36.6%
Statements ≥16%: 39.1%
bridge
Lines
Stmts
Funcs
Branch
6000/15235 lines covered
plugin
Lines
Stmts
Funcs
Branch
1229/3548 lines covered
shared
Lines
Stmts
Funcs
Branch
815/1415 lines covered
hooks
Lines
Stmts
Funcs
Branch
225/269 lines covered
| File | Stmts | Branch | Funcs | Lines | |
|---|---|---|---|---|---|
| bridge/src/check-deps.ts | 0% | 0% | 0% | 0% | |
| bridge/src/daemon-ws-client.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/hook-migration.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/session-transcript-timeline.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/apme/dashboard-html.ts | 0% | 100% | 0% | 0% | |
| bridge/src/apme/types.ts | 0% | 0% | 0% | 0% | |
| bridge/src/idotmatrix/idotmatrix-discover.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/adb-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/broadcast-module.ts | 0% | 100% | 0% | 0% | |
| bridge/src/modules/idotmatrix-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/index.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/mdns-module.ts | 0% | 100% | 0% | 0% | |
| bridge/src/modules/pixoo-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/serial-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/timebox-module.ts | 0% | 0% | 0% | 0% | |
| bridge/src/modules/types.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/daemon-server.ts | 0% | 1% | 1% | 0% | |
| bridge/src/voice.ts | 1% | 0% | 0% | 1% | |
| bridge/src/wake-word.ts | 2% | 0% | 0% | 2% | |
| bridge/src/voice-assistant.ts | 2% | 0% | 0% | 2% | |
| bridge/src/apme/index.ts | 2% | 0% | 0% | 2% | |
| bridge/src/pixoo/pixoo-discover.ts | 2% | 11% | 12% | 3% | |
| bridge/src/modules/d200h-module.ts | 3% | 0% | 0% | 3% | |
| bridge/src/adb-reverse.ts | 3% | 0% | 0% | 4% | |
| bridge/src/pixoo/pixoo-font.ts | 3% | 0% | 0% | 4% | |
| bridge/src/whisper-server-manager.ts | 4% | 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/session-focus-relay.ts | 5% | 0% | 0% | 5% | |
| bridge/src/broadcast.ts | 5% | 0% | 0% | 6% | |
| bridge/src/tui/dashboard.ts | 5% | 11% | 3% | 6% | |
| bridge/src/mlx-probe.ts | 6% | 0% | 0% | 6% | |
| bridge/src/timebox/timebox-daemon-sync.ts | 5% | 0% | 0% | 6% | |
| bridge/src/cli.ts | 6% | 0% | 1% | 7% | |
| bridge/src/wifi-config.ts | 6% | 0% | 0% | 7% | |
| bridge/src/apme/outcome.ts | 8% | 2% | 8% | 8% | |
| bridge/src/idotmatrix/idotmatrix-settings.ts | 7% | 0% | 0% | 8% | |
| bridge/src/pixoo/pixoo-settings.ts | 7% | 0% | 0% | 8% | |
| bridge/src/pixoo/pixoo-client.ts | 10% | 0% | 0% | 10% | |
| bridge/src/d200h/image-renderer.ts | 10% | 9% | 16% | 10% | |
| bridge/src/timebox/timebox-settings.ts | 8% | 6% | 8% | 10% | |
| bridge/src/pixoo/pixoo-camera.ts | 11% | 6% | 6% | 11% | |
| bridge/src/d200h/hid-protocol.ts | 11% | 0% | 0% | 12% | |
| bridge/src/mdns.ts | 14% | 28% | 25% | 12% | |
| bridge/src/pixoo/pixoo-bridge.ts | 11% | 0% | 0% | 12% | |
| bridge/src/logger.ts | 17% | 10% | 14% | 14% | |
| bridge/src/timeline-summarizer.ts | 13% | 0% | 0% | 15% | |
| bridge/src/idotmatrix/idotmatrix-daemon-sync.ts | 13% | 0% | 0% | 15% | |
| bridge/src/pixoo/pixoo-sprites.ts | 14% | 0% | 12% | 15% | |
| bridge/src/gateway-probe.ts | 12% | 0% | 0% | 16% | |
| bridge/src/foundation-models-helper.ts | 24% | 16% | 36% | 24% | |
| bridge/src/usage-api.ts | 23% | 13% | 50% | 24% | |
| bridge/src/esp32-serial.ts | 27% | 23% | 22% | 28% | |
| bridge/src/display-monitor.ts | 27% | 30% | 28% | 28% | |
| bridge/src/pixoo/pixoo-renderer.ts | 30% | 30% | 32% | 32% | |
| bridge/src/session-activity.ts | 29% | 30% | 42% | 34% | |
| bridge/src/codex-auth.ts | 36% | 8% | 25% | 37% | |
| bridge/src/bridge-core.ts | 38% | 41% | 26% | 40% | |
| bridge/src/codex-rate-limits.ts | 43% | 55% | 55% | 43% | |
| bridge/src/usage-tracker.ts | 44% | 55% | 23% | 44% | |
| bridge/src/opencode-client.ts | 40% | 38% | 57% | 44% | |
| bridge/src/adapters/opencode-adapter.ts | 41% | 29% | 35% | 44% | |
| bridge/src/ble-sync-spawn.ts | 43% | 30% | 38% | 46% | |
| bridge/src/model-catalog.ts | 39% | 17% | 36% | 46% | |
| bridge/src/windows-service.ts | 50% | 83% | 44% | 52% | |
| bridge/src/passive-observer.ts | 48% | 42% | 43% | 54% | |
| bridge/src/hook-server.ts | 53% | 29% | 54% | 55% | |
| bridge/src/ws-server.ts | 56% | 33% | 55% | 55% | |
| bridge/src/pty-manager.ts | 55% | 31% | 53% | 56% | |
| bridge/src/apme/antigravity-transcript.ts | 63% | 69% | 33% | 62% | |
| bridge/src/auth.ts | 59% | 37% | 75% | 63% | |
| bridge/src/adapters/openclaw.ts | 62% | 48% | 59% | 65% | |
| bridge/src/adapters/claude-code.ts | 71% | 40% | 46% | 71% | |
| bridge/src/session-registry.ts | 69% | 58% | 62% | 71% | |
| bridge/src/apme/classifier.ts | 65% | 66% | 74% | 71% | |
| bridge/src/adapters/index.ts | 71% | 67% | 100% | 71% | |
| bridge/src/tui/renderer.ts | 70% | 55% | 94% | 72% | |
| bridge/src/usage-event.ts | 76% | 72% | 100% | 73% | |
| bridge/src/apme/recommend.ts | 71% | 39% | 67% | 74% | |
| bridge/src/tui/terrarium.ts | 72% | 56% | 78% | 74% | |
| bridge/src/tui/ansi.ts | 74% | 49% | 84% | 75% | |
| bridge/src/apme/claude-transcript-reader.ts | 72% | 63% | 75% | 76% | |
| bridge/src/apme/store.ts | 68% | 71% | 83% | 80% | |
| bridge/src/session-aggregator.ts | 81% | 65% | 89% | 81% | |
| bridge/src/adapters/monitor.ts | 81% | 60% | 87% | 81% | |
| bridge/src/apme/runner.ts | 76% | 64% | 72% | 82% | |
| bridge/src/adapters/pty-adapter.ts | 82% | 81% | 65% | 82% | |
| bridge/src/state-machine.ts | 83% | 64% | 90% | 83% | |
| bridge/src/adapters/codex-cli.ts | 83% | 100% | 75% | 83% | |
| bridge/src/apme/sample-to-timeline.ts | 79% | 61% | 100% | 83% | |
| bridge/src/apme/http.ts | 81% | 77% | 88% | 83% | |
| bridge/src/session-timeline-relay.ts | 80% | 62% | 71% | 84% | |
| bridge/src/apme/adapters/codex-hook.ts | 79% | 60% | 100% | 85% | |
| bridge/src/apme/adapters/codex-turn-manager.ts | 78% | 58% | 95% | 86% | |
| bridge/src/apme/collector.ts | 83% | 71% | 90% | 87% | |
| bridge/src/pixoo/micro-glyphs.ts | 91% | 67% | 100% | 93% | |
| bridge/src/output-parser.ts | 90% | 84% | 100% | 94% | |
| bridge/src/apme/adapters/timeline.ts | 95% | 80% | 100% | 94% | |
| bridge/src/codex-output-parser.ts | 90% | 84% | 93% | 94% | |
| bridge/src/apme/classify-turn.ts | 91% | 80% | 100% | 95% | |
| bridge/src/utils/project-name.ts | 93% | 85% | 100% | 96% | |
| bridge/src/fallback-task-timeline.ts | 97% | 90% | 100% | 97% | |
| bridge/src/timeline-store.ts | 95% | 83% | 100% | 97% | |
| bridge/src/apme/adapters/openclaw-hook.ts | 95% | 73% | 100% | 97% | |
| bridge/src/apme/adapters/opencode-hook.ts | 98% | 84% | 100% | 98% | |
| bridge/src/awaiting-overlay.ts | 100% | 95% | 100% | 100% | |
| bridge/src/display-dim.ts | 95% | 69% | 100% | 100% | |
| bridge/src/openclaw-session.ts | 100% | 100% | 100% | 100% | |
| bridge/src/permission-resolver.ts | 98% | 68% | 100% | 100% | |
| bridge/src/apme/pareto.ts | 100% | 81% | 100% | 100% | |
| bridge/src/apme/settings.ts | 97% | 85% | 100% | 100% | |
| bridge/src/apme/adapters/claude-hook.ts | 100% | 77% | 100% | 100% | |
| bridge/src/apme/adapters/claude-pty.ts | 100% | 88% | 100% | 100% | |
| bridge/src/apme/scorers/index.ts | 98% | 73% | 100% | 100% | |
| bridge/src/tui/gauge.ts | 98% | 96% | 100% | 100% | |
| hooks/src/install.ts | 63% | 67% | 77% | 65% | |
| hooks/src/codex-install.ts | 84% | 62% | 87% | 87% | |
| hooks/src/codex-mini-toml.ts | 97% | 94% | 100% | 100% | |
| plugin-ulanzi/src/reconnect-supervisor.ts | 85% | 68% | 75% | 89% | |
| plugin/src/agent-link.ts | 0% | 0% | 0% | 0% | |
| plugin/src/encoder-registry.ts | 0% | 0% | 0% | 0% | |
| plugin/src/label-summarizer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/plugin.ts | 0% | 0% | 0% | 0% | |
| plugin/src/project-picker.ts | 0% | 0% | 0% | 0% | |
| plugin/src/project-scanner.ts | 0% | 0% | 0% | 0% | |
| plugin/src/timeline-store.ts | 0% | 0% | 0% | 0% | |
| plugin/src/voice-local.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/iterm-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/utility-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/actions/voice-dial.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/agent-logos.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/session-slot-renderer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/renderers/text-utils.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/apme.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/brightness.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/darkmode.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/diag.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/index.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/macos.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/media.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/mic.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/permission-mode.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/timer.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/tower.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/types.ts | 0% | 0% | 0% | 0% | |
| plugin/src/utility-modes/volume.ts | 0% | 0% | 0% | 0% | |
| plugin/src/bridge-client.ts | 55% | 35% | 67% | 59% | |
| plugin/src/renderers/timeline-renderer.ts | 58% | 34% | 75% | 63% | |
| plugin/src/log.ts | 59% | 50% | 33% | 67% | |
| plugin/src/session-slot-manager.ts | 77% | 73% | 75% | 79% | |
| plugin/src/utility-modes/usage.ts | 74% | 74% | 56% | 79% | |
| plugin/src/renderers/display-tile.ts | 77% | 50% | 100% | 80% | |
| plugin/src/renderers/usage-dial-renderer.ts | 75% | 55% | 91% | 80% | |
| plugin/src/renderers/button-renderer.ts | 82% | 72% | 100% | 82% | |
| plugin/src/renderers/response-renderer.ts | 85% | 40% | 92% | 87% | |
| plugin/src/connection-manager.ts | 85% | 79% | 81% | 88% | |
| plugin/src/renderers/voice-renderer.ts | 88% | 57% | 87% | 90% | |
| plugin/src/renderers/option-renderer.ts | 92% | 69% | 100% | 93% | |
| plugin/src/layout-manager.ts | 95% | 92% | 100% | 95% | |
| plugin/src/renderers/utility-renderer.ts | 98% | 57% | 100% | 98% | |
| plugin/src/center-slot.ts | 100% | 100% | 100% | 100% | |
| plugin/src/renderers/qr-renderer.ts | 100% | 90% | 100% | 100% | |
| plugin/src/renderers/usage-gauge.ts | 99% | 76% | 100% | 100% | |
| shared/src/adapter.ts | 0% | 100% | 100% | 0% | |
| shared/src/command-builders.ts | 0% | 100% | 0% | 0% | |
| shared/src/eval-schema.ts | 0% | 100% | 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/pricing.ts | 0% | 0% | 0% | 0% | |
| shared/src/sample.ts | 0% | 0% | 0% | 0% | |
| shared/src/telemetry-envelope.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/svg-renderers/text-utils.ts | 11% | 0% | 29% | 12% | |
| shared/src/svg-renderers/agent-logos.ts | 29% | 30% | 31% | 30% | |
| shared/src/svg-renderers/session-slot-renderer.ts | 38% | 28% | 48% | 38% | |
| shared/src/state-colors.ts | 38% | 50% | 67% | 42% | |
| shared/src/format-utils.ts | 46% | 45% | 38% | 46% | |
| shared/src/d200h-layout.ts | 48% | 44% | 58% | 48% | |
| shared/src/timeline-summarizer.ts | 73% | 63% | 57% | 76% | |
| shared/src/timeline-icons.ts | 85% | 78% | 75% | 81% | |
| shared/src/timeline.ts | 77% | 72% | 100% | 83% | |
| shared/src/session-utils.ts | 90% | 73% | 100% | 89% | |
| shared/src/connection-status.ts | 100% | 100% | 100% | 100% | |
| shared/src/design-tokens.ts | 100% | 100% | 100% | 100% | |
| shared/src/llm-settings.ts | 100% | 98% | 100% | 100% | |
| shared/src/protocol.ts | 100% | 100% | 100% | 100% | |
| shared/src/states.ts | 100% | 100% | 100% | 100% | |
| shared/src/timeline-markdown.ts | 99% | 92% | 100% | 100% |