* fix: Channel HTTP: server doesn't start after config change (no hot-r… (#779)
* fix: Channel HTTP: server doesn't start after config change (no hot-reload)
* review fixes
* review fixes
* fix linter
* fix code style
* fix: prevent session lock contention blocking message processing (#783)
* fix: prevent session lock contention blocking message processing
## Problem
After container restart, POST /api/chat/send returns 202 ACCEPTED but messages
don't appear in conversation_messages and agent never responds. Messages get
stuck in "stale state" after restart.
Root cause: Session lock was held for entire duration of chat_threads_handler
and chat_history_handler, including during slow database queries. This blocked
the agent loop from acquiring the session lock to process incoming messages,
causing them to hang indefinitely.
## Solution
1. **Release session lock early in chat_threads_handler**: Only acquire lock
when reading active_thread at response time, not during DB queries for
thread list. DB operations no longer block message processing.
2. **Release session lock early in chat_history_handler**: Only acquire lock
when accessing in-memory thread state, not during paginated DB queries or
thread ownership checks. DB operations no longer block message processing.
3. **Add comprehensive logging**: Track message flow from receipt through
session resolution, thread hydration, and state transitions. Helps diagnose
future issues:
- Message queued to agent loop (chat_send_handler)
- Processing message from channel (handle_message)
- Hydrating thread from DB (maybe_hydrate_thread)
- Resolving session and thread (resolve_thread)
- Checking thread state (process_user_input)
- Persisting user message (persist_user_message)
## Impact
- Message processing no longer blocks on session lock contention
- API response times for thread list/history queries unaffected (DB queries
still happen, but lock is not held)
- Better diagnostics for future debugging
## Testing
- All 2756 tests pass
- Code compiles with zero clippy warnings
- No changes to user-facing API or behavior, only lock timing
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* security: redact PII from info-level logs
Downgrade user_id and channel logging to debug level to prevent exposing
Personally Identifiable Information (PII) in production logs.
The user_id field can contain sensitive information such as phone numbers
(e.g., for Signal messages). Logging PII in cleartext at the info level
creates a security and privacy risk, as these logs may be stored in
persistent storage, indexed by log management systems, or accessible to
unauthorized personnel.
Changes:
- Info level: logs only message_id (UUID) for tracking
- Debug level: logs user_id, channel, thread_id for troubleshooting
This maintains debugging capability for developers while protecting user
privacy in production logs.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
---------
Co-authored-by: Claude Haiku 4.5 <[email protected]>
* chore: sync main into staging (#855)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)
Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).
- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini
Co-authored-by: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* fix: Chat input is hidden in mobile browser mode (#877)
* fix: stop XML-escaping tool output content (#598) (#874)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)
Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).
- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: stop XML-escaping tool output content in wrap_for_llm (#598)
Remove content escaping that corrupted JSON in tool output. The
<tool_output> structural boundary is preserved but content now passes
through raw, fixing downstream parse failures.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(safety): allow empty string tool params (#848)
* fix(safety): allow empty string tool params
* fix(safety): preserve heuristic checks and add path context to tool validation
This follow-up refactor addresses PR review feedback by restoring
heuristic checks (whitespace ratio, character repetition) for tool
parameter validation and improving error reporting.
Changes:
- Restored heuristic warnings in validate_non_empty_input so they apply
to both user input and tool parameters (when non-empty).
- Refactored check_strings to recursively build and pass JSON paths
(e.g., "metadata.tags[1]").
- Updated validation errors to use the specific JSON path as the field
name instead of the generic "input".
- Added regression tests for whitespace/repetition warnings and JSON
path reporting in tool parameters.
This ensures the safety layer remains semantically neutral about empty
strings (fixing the memory_tree path: "" issue) while maintaining
rigorous protection and providing better developer ergonomics.
* style: run cargo fmt
* perf: optimize release and dist build profiles (#843)
* perf: optimize release and dist build profiles
Add [profile.release] with strip=true and panic="abort" for smaller,
faster release binaries. Upgrade [profile.dist] from lto="thin" to
lto="fat" with codegen-units=1 for maximum optimization in CI releases.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove panic=abort from release profile
Reviewers (zmanian, Copilot, Gemini) correctly flagged that panic=abort
in the release profile would kill the entire process on any tokio task
panic, breaking fault isolation for the long-running server. Removed
from release profile entirely.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add PR template with risk assessment (#837)
* feat: add PR template with risk assessment and review tracks
Add a pull request template that includes summary, change type,
validation checklist, security/database impact sections, blast radius,
and rollback plan. Update CONTRIBUTING.md with review track definitions
(A/B/C) based on change risk level.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: expand CONTRIBUTING.md with setup, workflow, and guidelines
Add getting started, development workflow, code style summary,
database change guidance, and dependency management sections.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add fuzzing targets for untrusted input parsers (#835)
* feat: add fuzzing targets for untrusted input parsers
Add cargo-fuzz infrastructure with 5 fuzz targets exercising
security-critical code paths:
- fuzz_safety_sanitizer: Aho-Corasick + regex injection detection
- fuzz_safety_validator: Input validation (length, encoding, patterns)
- fuzz_leak_detector: Secret leak scanning (API keys, tokens)
- fuzz_tool_params: Tool parameter JSON validation
- fuzz_config_env: TOML/JSON config parsing
Each target exercises real IronClaw business logic with invariant
assertions. Includes corpus directories and setup documentation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: improve fuzz targets to exercise real IronClaw code paths
- fuzz_config_env: exercise SafetyLayer end-to-end (sanitize, validate,
policy check) instead of generic TOML/JSON parsing
- fuzz_tool_params: add validate_tool_schema coverage alongside
validate_tool_params
- Add "fuzz" to workspace exclude in root Cargo.toml
- Update README descriptions to match actual target behavior
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: replace redundant detect() call with meaningful invariant assertion
Replace the double sanitize()+detect() call with an assertion that
critical severity warnings always trigger content modification.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: rewrite fuzz_config_env to exercise IronClaw safety code directly
Replace SafetyLayer wrapper usage with direct Sanitizer, Validator, and
LeakDetector instantiation and invocation. Adds meaningful consistency
assertions (non-empty output, valid-means-no-errors, scan/clean agreement).
Removes the config construction that was only exercising struct instantiation.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(wasm): run leak scan before credential injection in tools wrapper (#791)
* fix(wasm): run leak scan before credential injection in tools wrapper
The tools WASM wrapper runs the LeakDetector on HTTP request headers
AFTER inject_host_credentials() has already substituted real secrets
(e.g., xoxb- Slack bot tokens). This causes the leak detector to
flag the tool's own legitimate outbound API calls as secret exfiltration.
Move the scan to run on raw_headers before any credential injection,
matching the fix already applied to the channels wrapper in #421.
Fixes the same class of bug as #421 (which only fixed channels/wasm/wrapper.rs).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* perf: inline leak scan to avoid Vec allocation on every HTTP request
Address review feedback: instead of cloning all header keys/values into
a Vec to pass to scan_http_request(), iterate over raw_headers directly
using scan_and_clean(). This also provides more specific error messages
(URL vs header vs body).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix cargo fmt formatting in leak scan loop
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(setup): drain residual terminal events before secret input (#747) (#849)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)
Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).
- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: skip the regression check
[skip-regression-check]
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* feat(agent): add context size logging before LLM prompt (#810)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(agent): add context size logging before LLM prompt
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* fix: preserve text before tool-call XML in forced-text responses (#852)
* fix: preserve text before tool-call XML in forced-text responses (#789)
Local models (Qwen3, DeepSeek, GLM) emit <tool_call> XML even when no
tools are available (force_text mode). The existing strip_xml_tag()
discards everything from an unclosed opening tag onward, producing an
empty string that triggers the "I'm not sure how to respond" fallback.
Add truncate_at_tool_tags() — a code-region-aware pre-processing step
that truncates at the first tool-call XML tag BEFORE clean_response()
runs, preserving all useful text before the tag. Protect all 7
clean_response() call sites. Case-insensitive matching handles models
that emit <TOOL_CALL> or <Tool_Call> variants.
Secondary fix: add has_native_thinking() model detection to skip
<think>/<final> system prompt injection for models with built-in
reasoning (Qwen3, QwQ, DeepSeek-R1, GLM-Z1, etc.), preventing
thinking-only responses that clean to empty.
Wire with_model_name(active_model_name()) at all 9 production sites
that construct Reasoning, so the runtime model name (not static config)
drives system prompt generation.
126 new/updated tests covering truncation edge cases, code-block
awareness, Unicode, case-insensitivity, StubLlm integration for
complete/plan/evaluate_success/respond_with_tools paths, model
detection, and conditional system prompt generation.
Closes #789
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address Copilot review — unclosed-only truncation, ASCII case folding
- truncate_at_tool_tags() now only truncates at UNCLOSED tool tags;
properly closed tags (e.g. <tool_call>...</tool_call>) are left intact
for clean_response() to strip normally, preserving any text after them
- Switch from to_lowercase() to to_ascii_lowercase() to prevent byte
offset misalignment with non-ASCII characters whose lowercase form
has different byte length (e.g. Kelvin sign U+212A)
- Add closing_tag_for() helper to derive closing tags from open patterns
- Fix doc comment: "fenced markdown code blocks or inline code spans"
(not "indented", which find_code_regions() doesn't detect)
- Add regression tests: closed vs unclosed for each tag variant,
Unicode + case-insensitive offset safety, and mixed closed/unclosed
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: minor review items — consistent ascii_lowercase, closing_tag_for tests
- Switch has_native_thinking() from to_lowercase() to to_ascii_lowercase()
for consistency with truncate_at_tool_tags() approach
- Add unit tests for closing_tag_for(): standard tags, space-suffixed
patterns, pipe-delimited tags, and exhaustive coverage of all
TOOL_TAG_PATTERNS entries
- Add test for mixed closed+unclosed tags of different types
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* Feat/docker shell edition (#804)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(mcp): strip top-level null params before forwarding to MCP servers (#795)
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)
Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).
- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(mcp): strip top-level null params before forwarding to MCP servers
LLMs frequently emit `"field": null` for optional parameters in tool
calls. Many MCP servers reject explicit nulls for fields that should
simply be absent — e.g. Notion returns 400 for `"sort": null` in a
search call, expecting the field to be omitted entirely.
Strip top-level null keys from the params object before calling
`call_tool()`. Only top-level keys are stripped; nested nulls are
preserved since they may be semantically meaningful.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* Add event-triggered routines and workflow skill templates (#756)
* Add event-triggered routines and workflow skill templates
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback for event_emit security and quality
Security fixes:
- Require approval (UnlessAutoApproved) for event_emit, matching routine_fire
- Enable sanitization on event_emit payload (external JSON reaches LLM)
- Remove user_id parameter from event_emit to prevent IDOR — always use ctx.user_id
Correctness fixes:
- Rename source → event_source in event_emit for consistency with routine_create
- Use json_value_as_filter_string for filter parsing (handles numbers/booleans)
- Case-insensitive matching for event source and event_type
- Add debug logging for missing filter keys in payload
- Fix skill_install_routine_webhook_sim test missing .with_skills()
- Fix schema_validator test for event_emit payload properties
Code quality:
- Move EventEmitTool struct/impl after RoutineHistoryTool (fix split layout)
- Deduplicate routine_to_info into RoutineInfo::from_routine in types.rs
- Add test section headers in e2e_routine_heartbeat.rs
- Clarify event_emit description to specify system_event routines only
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: make routine_system_event_emit test create routine before emitting
- Add routine_create step to trace fixture so event_emit has a matching
routine to fire
- Assert fired_routines > 0, not just key presence (Copilot review)
- Add .with_auto_approve_tools(true) since event_emit now requires approval
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: renumber test headers after system_event test insertion
Test 4 was duplicated (routine_cooldown and heartbeat_findings).
Renumber heartbeat_findings to Test 5 and heartbeat_empty_skip to Test 6.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: merge staging and add missing RoutineEngine args in test
RoutineEngine::new on staging requires `tools` and `safety` params.
Update system_event_trigger_matches_and_filters test to pass them.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address new Copilot review comments
- Add .with_auto_approve_tools(true) to skill_install_routine_webhook_sim
test so event_emit doesn't block on approval
- Fix module-level doc comment for event_emit to specify system_event trigger
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: deduplicate json_value_as_string helper
Remove private `json_value_as_string` from routine_engine.rs and use
the identical public `json_value_as_filter_string` from routine.rs,
eliminating divergence risk. (Copilot review)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix: enable WASM credential injection in No-DB environments (#845)
* fix(wasm): enable credential injection in no-DB environments via env var fallback
When a secrets store is unavailable (e.g. no-DB mode), WASM channel
credentials were silently not injected, causing channels to start without
credentials. Fix by:
- Changing `inject_channel_credentials_from_secrets` to accept
`Option<&dyn SecretsStore>` — secrets store is tried first when present
- Adding env var fallback (`inject_env_credentials`) for credentials not
covered by the secrets store
- Enforcing a channel-name prefix security check on env var names to
prevent WASM channels from reading unrelated host credentials
(e.g. `AWS_SECRET_ACCESS_KEY`)
- Extracting pure `resolve_env_credentials` helper for testability
- Adding case-insensitive prefix matching for secrets store lookup
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(wasm): inject credentials at startup when no secrets store (setup.rs path)
The startup path (setup_wasm_channels -> register_channel) was guarded by
`if let Some(secrets) = secrets_store`, so in No-DB mode credentials were
never injected and the channel started without them.
Fix by:
- Changing inject_channel_credentials to accept Option<&dyn SecretsStore>
- Always calling it (removing the if-let guard) — env var fallback runs
even when secrets_store is None
- Adding channel-name prefix security check to the env var fallback path
(e.g. TELEGRAM_ for channel "telegram"), consistent with manager.rs
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(test): correct misleading comment on ICTEST1_UNRELATED_OTHER placeholder
* fix(wasm): guard against empty channel name in credential injection
An empty channel_name would produce prefix "_", allowing any env var
starting with "_" to pass the security check and be injected. Add an
early-return guard in resolve_env_credentials, inject_env_credentials,
and inject_channel_credentials. Add a test to cover this path.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: lizican123 <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix: promote to main (#878)
* fix: replace unsafe env::set_var with thread-safe inject_single_var in SIGHUP handler
Fixes race condition where SIGHUP handler modifies global environment variables
while other threads may be reading them via Config::from_env().
Changes:
- Replace unsafe { std::env::set_var() } with ironclaw::config::inject_single_var()
- Uses INJECTED_VARS mutex instead of unsafe global state modification
- All reads via optional_env() check the thread-safe overlay first
- Prevents data races between SIGHUP reload and concurrent config reads
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: spawn webhook restart as background task to avoid blocking I/O across lock
Prevents holding Mutex lock during async I/O operations (TcpListener::bind,
task shutdown). The SIGHUP handler no longer blocks webhook processing during
listener restart.
Changes:
- Read old_addr and drop lock immediately
- Spawn restart_with_addr() as background task via tokio::spawn
- Lock is only held during the actual restart operation, not the signal handler
Benefits:
- SIGHUP handler returns immediately without blocking
- Webhook requests not delayed by listener restart I/O
- Lock contention significantly reduced
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: add graceful shutdown mechanism for SIGHUP handler background task
Prevents unbounded loop without cancellation token. The SIGHUP handler now
listens for a shutdown signal and exits cleanly during graceful termination.
Changes:
- Create broadcast channel for shutdown signaling
- SIGHUP handler uses tokio::select! to wait for shutdown or SIGHUP
- Send shutdown signal to all background tasks after agent.run() completes
- Ensures clean task lifecycle and no orphaned background tasks
Benefits:
- Proper task cancellation during graceful shutdown
- Follows Tokio best practices for background task management
- No background tasks orphaned when runtime shuts down
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* refactor: replace stringly-typed parameter filtering with typed enum and single helper
Fixes DRY violation where unsupported parameter filtering was duplicated across
rig_adapter.rs and anthropic_oauth.rs using string contains checks.
Changes:
- Add UnsupportedParam typed enum in provider.rs (Temperature, MaxTokens, StopSequences)
- Create strip_unsupported_completion_params() helper function
- Create strip_unsupported_tool_params() helper function
- Update rig_adapter.rs to use shared helpers
- Update anthropic_oauth.rs to use shared helpers
- Replace 60+ lines of duplicate stringly-typed logic
Benefits:
- Type safety: parameter names checked at compile time
- Single source of truth: adding a new param updates one place
- Reduced maintenance burden: no duplicate logic to keep in sync
- Better code clarity: named enum variant is self-documenting
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* docs: clarify intentional parameter asymmetry between completion and tool requests
Add documentation explaining why strip_unsupported_tool_params does not handle
StopSequences: the field doesn't exist in ToolCompletionRequest.
Changes:
- Add clarifying comments to strip_unsupported_tool_params()
- Explain why StopSequences is only in CompletionRequest
- Note that ToolCompletionRequest only supports Temperature and MaxTokens
- Inline comment confirms no action needed for StopSequences
This addresses the appearance of incomplete implementation without changing logic,
as the asymmetry is intentional and correct (ToolCompletionRequest lacks the field).
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* perf: isolate webhook_secret to reduce lock contention on hot path
Move webhook_secret from shared HttpChannelState RwLock into its own Arc<RwLock<>>.
This eliminates contention between secret validation and other state operations.
Changes:
- Change webhook_secret field type from RwLock<Option<SecretString>> to Arc<RwLock<Option<SecretString>>>
- Update initialization in HttpChannel::new()
- Update comments to explain isolation rationale
Benefits:
- Reduce lock contention on webhook request hot path (secret validation)
- Rarely-changing field (SIGHUP only) isolated from frequent state accesses
- Other state operations (tx, pending_responses) no longer wait behind secret reads
- Minimal code change: only field declaration and initialization
The Arc wrapper allows cloning the RwLock handle to separate concerns. With this
change, every webhook request acquires its own isolated lock for secret validation,
not the shared HttpChannelState lock. This scales better under high request volume.
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: prevent partial state corruption on SIGHUP restart failure
Ensure atomicity of configuration reload: if webhook listener restart fails,
secret update is skipped to prevent inconsistent state.
Changes:
- Wait for restart_with_addr() to complete (don't spawn background task)
- Track restart result with restart_failed flag
- Only update secret if restart succeeded or wasn't needed
- Ensure listener and secret stay synchronized
Problem addressed:
- Before: restart spawned as background task, secret updated immediately
- If restart failed, secret was changed but listener still on old address
- This left system in inconsistent state (partial corruption)
Solution:
- Make restart blocking (SIGHUP handler can wait, it's not on request hot path)
- Atomically update secret only after successful restart
- Flag prevents race between restart and secret update
Benefits:
- Configuration changes are atomic (both succeed or both fail together)
- No partial state corruption on restart failure
- Failed restarts don't silently leave inconsistent state
- Secret and listener address stay in sync
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* refactor: generalize hot-secret-swapping with ChannelSecretUpdater trait
Decouple SIGHUP handler from HTTP channel internals by introducing a trait
for channels that support zero-downtime secret updates.
Changes:
- Add ChannelSecretUpdater trait in channels/channel.rs
- Implement ChannelSecretUpdater for HttpChannelState
- Export trait from channels module
- Update SIGHUP handler to use trait-based secret updater collection
- Replace explicit HTTP channel knowledge with generic updater loop
Benefits:
- SIGHUP handler no longer depends on HttpChannelState details
- Tight coupling removed: main.rs doesn't need HTTP channel imports
- Extensible: new channels can opt-in by implementing the trait
- Scalable: multiple channels supported without main.rs changes
- Maintainable: adding channels requires only trait implementation, not SIGHUP handler edits
Pattern:
- ChannelSecretUpdater trait defines the interface for all updaters
- Channels that support hot-secret-swapping implement the trait
- SIGHUP handler loops through all registered updaters generically
Verification:
- All 2,787 tests pass
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* feat: validate parameter names at deserialization time, not just tests
Add custom serde deserializer for unsupported_params that validates parameter
names at runtime when loading providers.json (or user overrides).
Changes:
- Add unsupported_params_de module with custom deserializer
- Only allows: "temperature", "max_tokens", "stop_sequences"
- Invalid parameter names cause immediate deserialization error
- Update ProviderDefinition to use custom deserializer
- Enhanced test with explicit parameter name validation
- Add new test that verifies invalid parameters are rejected
Problem solved:
- Before: Invalid param names (e.g., "temperrature") silently ignored
- Now: Rejected at deserialization time with clear error message
- Prevents runtime failures caused by typos in configuration
Example error:
unsupported parameter name 'temperrature': must be one of: temperature, max_tokens, stop_sequences
Benefits:
- Fail-fast: errors caught when loading config, not at runtime
- Clear feedback: error message lists valid parameter names
- Type safety: validators run during deserialization
- Configuration errors detected immediately, not silently ignored
Verification:
- All 2,788 tests pass (including new validation test)
- Zero clippy warnings
- Code compiles successfully
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
---------
Co-authored-by: Claude Haiku 4.5 <[email protected]>
* merge: resolve conflicts for PR #800 and #822 into staging (#881)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: unify three agentic loops into single AgenticLoop engine (#654)
Replace three independent copy-pasted agentic loops (dispatcher, worker,
container runtime) with a single shared engine in `agentic_loop.rs` that
all consumers customize via the `LoopDelegate` trait.
Phase 1 — Shared engine (`src/agent/agentic_loop.rs`, 205 lines):
- `run_agentic_loop()` owns the core LLM → tool exec → repeat cycle
- `LoopDelegate` trait (Send + Sync, &dyn dispatch) with 6 hook points
- Tool intent nudge logic consolidated (was duplicated in 3 files)
- Iteration limit + force-text behavior preserved
Phase 2 — Three delegate implementations:
- `ChatDelegate` (dispatcher.rs): 3-phase approval flow, hooks, cost
guard, context compaction, skill attenuation, interruption
- `JobDelegate` (worker/job.rs): planning pre-loop phase, parallel
JoinSet exec, mark_completed/stuck/failed, SSE streaming, self-repair
- `ContainerDelegate` (worker/container.rs): sequential tool exec,
HTTP-proxied LLM, container-safe tools, credential injection
Phase 3 — File moves and cleanup:
- Delete `src/agent/worker.rs` — job logic moved to `src/worker/job.rs`
- Rename `src/worker/runtime.rs` → `src/worker/container.rs`
- Re-export `Worker`/`WorkerDeps` from `crate::worker` in `agent/mod.rs`
- Update `scheduler.rs` imports to new worker location
Shared helpers (`src/tools/execute.rs`):
- `execute_tool_with_safety()` replaces 4 copies of validate → timeout
→ execute → serialize
- `process_tool_result()` replaces 3 copies of sanitize → wrap →
ChatMessage (also used by thread_ops.rs approval resume paths)
Net result: -2,408 lines, zero duplicated loop logic, single code path
for tool intent nudge and completion detection.
Closes #654
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback from Copilot
1. scheduler.rs: Replace `unwrap_or` fallback with proper error
propagation when parsing tool output JSON — surfaces bugs instead
of silently changing the output type.
2. worker/job.rs: Drop MutexGuard before the cancellation `.await` in
`check_signals()` to avoid holding a lock across an async I/O call
(prevents `await_holding_lock` lint).
3. worker/job.rs: Restore consecutive rate-limit counter
(MAX_CONSECUTIVE_RATE_LIMITS = 10) so sustained rate limiting marks
the job stuck with "Persistent rate limiting" instead of silently
burning through max_iterations.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: incorporate staging changes — token budget tracking + mark_failed
Merge staging's changes into the refactored JobDelegate:
- Add token budget tracking in call_llm (update_context/add_tokens)
- mark_stuck → mark_failed for iteration cap and rate-limit exhaustion
(aligns with staging's #788 fix)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address zmanian's PR review — eliminate type erasure, clean up
Address all 6 review points from zmanian on PR #800:
1. Replace LoopOutcome::Custom(Box<dyn Any>) with typed
LoopOutcome::NeedApproval(Box<PendingApproval>) — eliminates
type erasure and downcast, resolves clippy large_enum_variant.
2. Remove dead max_tool_iterations field from ChatDelegate struct.
3. Add on_tool_intent_nudge() hook to LoopDelegate trait with
implementations in Job and Container delegates for observability.
4. Fix SSE events in job worker to emit raw sanitized content
instead of XML-wrapped <tool_output> tags.
5. Remove 4 duplicate completion tests from job.rs that were
already covered by the shared util module.
6. Avoid logging full tool results — use result_size_bytes in
debug logs (execute.rs, job.rs).
Also updates path references in CLAUDE.md, COVERAGE_PLAN.md,
and add-sse-event.md command.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat(doctor): expand diagnostics from 7 to 16 health checks
* test: add unit tests for agentic_loop and execute shared modules
Add 16 tests covering the two new critical shared modules:
agentic_loop.rs (10 tests):
- Text response exits loop immediately
- Tool call → text response continuation
- LoopSignal::Stop exits before LLM call
- LoopSignal::InjectMessage adds user message to context
- Max iterations terminates with LoopOutcome::MaxIterations
- Tool intent nudge fires twice then caps
- before_llm_call early exit bypasses LLM
- truncate_for_preview: short string, long string, multibyte safety
execute.rs (6 tests):
- execute_tool_with_safety success path
- Missing tool returns ToolError::NotFound
- Tool execution failure propagates
- Per-tool timeout enforcement (50ms)
- process_tool_result XML wrapping on success
- process_tool_result error formatting
All 2,777 unit tests pass, 0 clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address code review — 9 issues across agentic loop, job worker, container
CRITICAL fixes:
- Rate-limit exhaustion now returns Err(LlmError::RateLimited) instead of
Ok(Text("")), stopping the loop immediately with no ghost iteration.
Below-threshold retries still use Text("") with an explicit empty-string
guard in handle_text_response to skip injection.
- check_signals drains the entire message channel before returning,
prioritizing Stop over UserMessage. Previously returned early on first
UserMessage, silently dropping any queued Stop or additional messages.
- check_signals now detects all non-progressing job states (Cancelled,
Failed, Stuck, Completed, Submitted, Accepted) instead of only
Cancelled and Failed.
HIGH fixes:
- Error path in process_tool_result_job applies truncate_for_preview to
bound error strings in SSE/DB events (was unbounded).
- Document Send+Sync lifetime constraint on LoopDelegate trait.
- Test mock before_llm_call refactored from double-lock to single lock
acquisition, eliminating deadlock risk on refactor.
MEDIUM fixes:
- CompletionReport includes actual iteration count via shared
Arc<Mutex<u32>> tracker (was hardcoded 0).
- process_tool_result_job return type changed from Result<bool> to
Result<()> — the bool was always false (dead API).
- Deduplicate truncate in container.rs; now uses truncate_for_preview
from agentic_loop.
Verified: 0 clippy warnings, 2781 tests pass, cargo fmt clean.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Umesh Kumar Singh <[email protected]>
Co-authored-by: reidliu41 <[email protected]>
* Revert "Feat/docker shell edition" + fix fmt/clippy (#886)
* Revert "Feat/docker shell edition (#804)"
This reverts commit c566faf28f.
* style: fix formatting issues from revert
Run cargo fmt to fix formatting across 7 files after the revert of
the docker shell edition feature.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: centralize test credential constants into testing::credentials (#829)
* refactor: centralize test credential constants into testing::credentials
Scattered test credential strings (API keys, OAuth tokens, crypto keys,
Telegram tokens, session tokens) across ~25 files made security auditing
harder and created unnecessary duplication. Centralize all test-only fake
credentials into a new `src/testing/credentials.rs` module with named
constants and a shared `test_secrets_store()` helper.
- Convert `src/testing.rs` to directory module (`src/testing/mod.rs`)
- Add `src/testing/credentials.rs` with ~30 named constants
- Replace hardcoded literals in 24 source files
- Deduplicate `test_store()` helper (was copy-pasted in 3 files)
- Leave leak_detector/shell/signature tests as-is (inline values
aid readability for pattern detection tests)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: replace real Telegram bot token with obviously fake test stub
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Update src/testing/credentials.rs
Co-authored-by: Copilot <[email protected]>
* Update src/testing/credentials.rs
Co-authored-by: Copilot <[email protected]>
* refactor: address PR review feedback on test credentials
- Fix TEST_CRYPTO_KEY doc comment ("32-byte hex" → "32-character key string")
- Rename confusing "real"/"fake" Anthropic constant names and values
- Change TEST_STRIPE_KEY from "sk-live" to "sk_test_fake123" to avoid scanners
- Use test_secrets_store() helper in orchestrator and http tool tests
- Clarify config_round_trip.rs doc comment about integration test visibility
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Copilot <[email protected]>
* fix(registry): version-pinned WASM artifact URLs + ChecksumMismatch source fallback (#832)
* fix(registry): version-pinned WASM artifact URLs + ChecksumMismatch fallback (#439)
Root cause: all artifact URLs used releases/latest/download/, which is a
moving target. Every release rebuilds all WASM extensions non-deterministically,
so sha256 baked into an older binary diverges from the content at 'latest'.
ChecksumMismatch was also a hard block with no source-build fallback.
Three-layer fix:
1. src/registry/installer.rs — allow source-build fallback for ChecksumMismatch
on releases/latest URLs (moving-target artifact rotation, not tampering).
Version-pinned URLs (releases/download/vX.Y.Z/) remain a hard block.
Adds regression test (test_source_fallback_on_latest_url_mismatch) and
updates test_should_attempt_source_fallback_policy to cover both URL types.
2. .github/workflows/release.yml — three CI changes:
- build-wasm-extensions: version-detect, skip-if-unchanged, versioned filenames
(name-{version}-wasm32-wasip2.tar.gz). Skip rebuild when manifest already has
a non-null sha256 and the URL embeds the current version — stable checksums
until source actually changes.
- build-local-artifacts: patch manifests with version-pinned URL + sha256 (for
binary embedding via build.rs).
- update-registry-checksums: same URL patching for the main-branch PR.
All three sed patterns use '.*' (greedy) to correctly handle pre-release
version strings like 0.1.0-alpha.1.
3. registry/{tools,channels}/*.json — null out all 14 stale sha256 values.
Null sha256 -> MissingChecksum -> source-build fallback (works on all binaries).
Next release CI will populate version-pinned URLs + stable checksums.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* style: cargo fmt
* fix(ci): use JSON filename stem for WASM bundle names to fix manifest lookup
Manifests like registry/tools/slack.json have name='slack-tool', causing
the patching step to look for registry/tools/slack-tool.json (missing).
Introduce file_stem (JSON filename without .json) for the bundle filename
and checksums.txt entry, while keeping ext_name (manifest .name) for archive
contents — the installer extracts files by manifest.name so those must still
match. The patching step strips -{version}-wasm32-wasip2.tar.gz from the
filename stem and looks up registry/tools/slack.json correctly.
* fix(registry): tighten fallback URL check + deduplicate tests
Address PR review feedback:
1. Make should_attempt_source_fallback check repo-specific
(github.com/nearai/ironclaw/releases/latest/) instead of a
generic substring (/releases/latest/download/).
2. Remove duplicate ChecksumMismatch cases from
test_should_attempt_source_fallback_policy — that coverage
lives in the dedicated regression test
test_source_fallback_on_latest_url_mismatch.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix: agent logging (#888)
* fix: optimize agent logging to reduce DataDog bill
* fix: log permanent repair failures as ERROR not WARN
RepairResult::Failed is permanent failure requiring attention (ERROR level)
not a temporary/retryable condition (WARN level).
[skip-regression-check]
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* security: remove user message content from trace logs
Never log user message content at any log level (includes TRACE).
Log only safe metadata: content length, message ID, image count.
This prevents accidental exposure of sensitive user data in logs
even at the most verbose logging level.
[skip-regression-check]
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* security: move LLM response body logging to TRACE level
Response bodies can contain user-generated content, tool outputs, and
leaked secrets. Moving to TRACE (not enabled in production) prevents
exposure in DEBUG logs. Status log remains at DEBUG.
[skip-regression-check]
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* refactor: simplify URL sanitization using url::Url API
Use set_query, set_fragment, set_username, set_password methods
instead of manual string reconstruction. Cleaner, handles edge cases,
eliminates port branching complexity.
[skip-regression-check]
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* test: add comprehensive unit tests for sanitize_url_for_logging
Add 9 test cases covering:
- URL with query parameters
- URL with credentials (user:pass@host)
- URL with fragment
- URL with port
- URL with all components combined
- Malformed URL fallback behavior
- Short strings (pass-through)
- Non-URL-like strings
- Path preservation
Tests verify that sanitization correctly removes sensitive components
while preserving safe components like host, port, and path.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: libsql per-migration logs should be DEBUG, not TRACE
Individual migration logs are now visible with standard debug logging
(RUST_LOG=ironclaw=debug), improving debuggability when troubleshooting
migration issues. Summary log remains at INFO level.
Fixes behavioral change that made it harder to identify which specific
migration ran or failed without enabling full TRACE logging.
[skip-regression-check]
---------
Co-authored-by: Claude Haiku 4.5 <[email protected]>
* fix: staging CI review issues (batch 1) (#883)
* fix: address staging-ci-review issues (batch 1)
- #811: Fix unreachable error handling in worker — restructure .await?
to explicit match on nested Result so token budget errors are properly
logged and marked as failed
- #813: Combine metadata + token budget into single update_context()
call to prevent concurrent worker observing partial state
- #814: Persist max_tokens and total_tokens_used to both PostgreSQL and
libSQL backends — add V12 migration, update save_job/get_job
- #815: Cap user-supplied max_tokens at configured max_tokens_per_job
to prevent budget bypass via metadata injection
- #869: Release locks before async I/O in webhook handler (http.rs) and
SIGHUP handler (main.rs) to prevent blocking concurrent requests
Fixes: #811, #813, #814, #815, #869
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #883 review feedback
- Fix min(user_val, 0) bug: guard for unlimited config (max_tokens_per_job == 0)
- Remove duplicate columns from libSQL base SCHEMA (v12 migration is sole source)
- Use get_i64() helper for consistency in libsql/jobs.rs
- Add regression tests for scheduler token budget capping
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: gate ChannelSecretUpdater import behind #[cfg(unix)] for Windows clippy
The import was unconditional but all usages are inside a #[cfg(unix)]
block, causing unused-import errors on Windows CI.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Nick Pismenkov <[email protected]>
Co-authored-by: Claude Haiku 4.5 <[email protected]>
Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Xing Ji <[email protected]>
Co-authored-by: Nick Stebbings <[email protected]>
Co-authored-by: Reid <[email protected]>
Co-authored-by: Umesh Kumar Singh <[email protected]>
Co-authored-by: 智方云cubecloud-io <[email protected]>
Co-authored-by: lizican <[email protected]>
Co-authored-by: lizican123 <[email protected]>
Co-authored-by: Zaki Manian <[email protected]>
Co-authored-by: reidliu41 <[email protected]>
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot <[email protected]>
32 KiB
IronClaw Coverage Plan: 63.3% to 95%
Generated 2025-03-06 from Codecov
Current State
| Metric | Value |
|---|---|
| Current coverage | 48,571 / 76,694 lines = 63.33% |
| Target | 72,859 / 76,694 lines = 95.0% |
| Gap | 24,288 lines need coverage |
| Files >= 95% | 43 / 239 |
| Files < 95% | 196 (27,872 total misses) |
Module Summary
Sorted by uncovered lines (descending):
| Module | Lines | Hits | Miss | Coverage | Priority |
|---|---|---|---|---|---|
channels/ |
14,079 | 8,677 | 5,402 | 61.6% | P0 |
tools/ |
13,445 | 9,407 | 4,038 | 70.0% | P1 |
agent/ |
9,152 | 6,096 | 3,056 | 66.6% | P0 |
setup/ |
3,005 | 462 | 2,543 | 15.4% | P1 |
extensions/ |
3,540 | 1,298 | 2,242 | 36.7% | P0 |
cli/ |
2,834 | 697 | 2,137 | 24.6% | P1 |
history/ |
1,626 | 0 | 1,626 | 0.0% | P0 |
llm/ |
7,029 | 5,776 | 1,253 | 82.2% | P2 |
(root) |
4,122 | 3,121 | 1,001 | 75.7% | P2 |
worker/ |
1,274 | 480 | 794 | 37.7% | P1 |
sandbox/ |
1,615 | 897 | 718 | 55.5% | P2 |
registry/ |
1,588 | 1,107 | 481 | 69.7% | P2 |
db/ |
921 | 441 | 480 | 47.9% | P1 |
workspace/ |
2,006 | 1,584 | 422 | 79.0% | P2 |
orchestrator/ |
1,199 | 795 | 404 | 66.3% | P2 |
config/ |
1,464 | 1,095 | 369 | 74.8% | P2 |
hooks/ |
1,379 | 1,081 | 298 | 78.4% | P2 |
secrets/ |
687 | 407 | 280 | 59.2% | P2 |
skills/ |
1,714 | 1,585 | 129 | 92.5% | P3 |
context/ |
693 | 586 | 107 | 84.6% | P3 |
estimation/ |
467 | 369 | 98 | 79.0% | P3 |
safety/ |
1,424 | 1,337 | 87 | 93.9% | P3 |
evaluation/ |
226 | 152 | 74 | 67.3% | P3 |
pairing/ |
498 | 446 | 52 | 89.6% | P3 |
tunnel/ |
391 | 368 | 23 | 94.1% | P3 |
observability/ |
316 | 307 | 9 | 97.2% | Done |
Top 40 Files by Uncovered Lines
These files account for the vast majority of the coverage gap:
| File | Lines | Miss | Coverage | Lines to 95% |
|---|---|---|---|---|
src/extensions/manager.rs |
2,404 | 2,083 | 13.3% | 1,962 |
src/setup/wizard.rs |
2,150 | 1,789 | 16.8% | 1,681 |
src/history/store.rs |
1,486 | 1,486 | 0.0% | 1,411 |
src/channels/web/server.rs |
1,985 | 993 | 50.0% | 893 |
src/channels/wasm/wrapper.rs |
2,237 | 934 | 58.2% | 822 |
src/agent/thread_ops.rs |
1,044 | 763 | 26.9% | 710 |
src/cli/tool.rs |
757 | 735 | 2.9% | 697 |
src/setup/channels.rs |
645 | 596 | 7.6% | 563 |
src/agent/commands.rs |
587 | 587 | 0.0% | 557 |
src/main.rs |
740 | 522 | 29.4% | 485 |
src/channels/web/handlers/jobs.rs |
513 | 456 | 11.1% | 430 |
src/tools/builder/core.rs |
524 | 456 | 13.0% | 429 |
src/worker/job.rs |
1,078 | 467 | 56.7% | 413 |
src/channels/web/handlers/chat.rs |
564 | 417 | 26.1% | 388 |
src/tools/wasm/wrapper.rs |
1,005 | 436 | 56.6% | 385 |
src/channels/signal.rs |
1,814 | 472 | 74.0% | 381 |
src/tools/mcp/auth.rs |
472 | 378 | 19.9% | 354 |
src/worker/container.rs |
350 | 330 | 5.7% | 312 |
src/tools/builtin/job.rs |
1,014 | 359 | 64.6% | 308 |
src/cli/mcp.rs |
322 | 319 | 0.9% | 302 |
src/cli/oauth_defaults.rs |
730 | 335 | 54.1% | 298 |
src/llm/nearai_chat.rs |
854 | 340 | 60.2% | 297 |
src/sandbox/container.rs |
407 | 317 | 22.1% | 296 |
src/tools/mcp/client.rs |
341 | 291 | 14.7% | 273 |
src/registry/installer.rs |
765 | 311 | 59.3% | 272 |
src/orchestrator/job_manager.rs |
405 | 270 | 33.3% | 249 |
src/channels/web/handlers/routines.rs |
249 | 249 | 0.0% | 236 |
src/agent/scheduler.rs |
559 | 263 | 53.0% | 235 |
src/tools/wasm/storage.rs |
296 | 243 | 17.9% | 228 |
src/channels/repl.rs |
233 | 233 | 0.0% | 221 |
src/llm/session.rs |
413 | 242 | 41.4% | 221 |
src/worker/claude_bridge.rs |
629 | 247 | 60.7% | 215 |
src/agent/agent_loop.rs |
523 | 234 | 55.2% | 207 |
src/worker/api.rs |
258 | 207 | 19.8% | 194 |
src/sandbox/proxy/http.rs |
307 | 192 | 37.5% | 176 |
src/channels/wasm/storage.rs |
182 | 182 | 0.0% | 172 |
src/cli/registry.rs |
177 | 177 | 0.0% | 168 |
src/llm/reasoning.rs |
1,163 | 219 | 81.2% | 160 |
src/tools/builder/testing.rs |
308 | 174 | 43.5% | 158 |
src/db/postgres.rs |
166 | 166 | 0.0% | 157 |
Tier 1 -- High-Impact Unit Tests (~8,500 lines)
Pure logic, serialization, and database queries testable in isolation without real infrastructure. Highest coverage gain per unit of effort.
src/history/store.rs -- 0% -> 95% (+1,411 lines)
PostgreSQL repository layer (conversations, jobs, actions, LLM calls, estimation
snapshots). Test query construction and result mapping. Can use the libSQL backend
as a real in-memory database or test doubles for the Database trait.
Tests to write:
test_store_conversation_crud-- create, read, update, delete conversationstest_store_job_lifecycle-- insert job, update status through state machinetest_store_action_recording-- record and query job actionstest_store_llm_call_tracking-- insert and aggregate LLM call recordstest_store_estimation_snapshots-- save and retrieve estimation data
src/history/analytics.rs -- 0% -> 95% (+133 lines)
Aggregation queries (JobStats, ToolStats). Test the query builders and result deserialization.
Tests to write:
test_job_stats_aggregation-- verify counts, durations, success ratestest_tool_stats_ranking-- verify tool usage frequency sortingtest_analytics_empty_db-- graceful handling of no data
src/extensions/manager.rs -- 13.3% -> 95% (+1,962 lines)
Largest single file gap. Extension lifecycle orchestration (install, auth, activate, remove), config parsing, and state transitions.
Tests to write:
test_extension_install_from_manifest-- parse manifest, create extension recordtest_extension_auth_flow-- OAuth token setup, credential storagetest_extension_activate_deactivate-- state transitions, tool registrationtest_extension_remove_cleanup-- remove extension, clean up artifactstest_extension_config_validation-- reject invalid configs, handle defaultstest_extension_list_filtering-- filter by status, type, search querytest_extension_capability_check-- verify required capabilities before activation
src/extensions/discovery.rs -- 27.8% -> 95% (+125 lines)
Extension discovery from filesystem and registry.
Tests to write:
test_discover_local_extensions-- scan directory, parse manifeststest_discover_skip_invalid-- gracefully skip malformed extension dirstest_discover_dedup-- handle duplicate extensions across paths
src/tools/builder/core.rs -- 13% -> 95% (+429 lines)
BuildRequirement, SoftwareType, Language types and project scaffolding.
Tests to write:
test_build_requirement_parsing-- deserialize from JSONtest_scaffold_project_structure-- verify generated file treetest_language_detection-- detect language from file extensionstest_software_type_constraints-- validate type-specific requirements
src/tools/builder/testing.rs -- 43.5% -> 95% (+158 lines)
Test harness integration for built tools.
Tests to write:
test_harness_setup_teardown-- lifecycle of test environmenttest_harness_run_tests-- execute tests and capture resultstest_harness_failure_reporting-- verify error details on test failure
src/tools/mcp/auth.rs -- 19.9% -> 95% (+354 lines)
OAuth token management for MCP servers.
Tests to write:
test_token_refresh_on_expiry-- auto-refresh when token expirestest_token_header_injection-- correct Authorization header formattest_token_persistence-- save/load tokens across restartstest_oauth_pkce_flow-- code verifier/challenge generationtest_auth_config_parsing-- parse various auth config formats
src/tools/mcp/client.rs -- 14.7% -> 95% (+273 lines)
JSON-RPC client for MCP protocol.
Tests to write:
test_jsonrpc_request_serialization-- correct JSON-RPC 2.0 formattest_jsonrpc_response_parsing-- handle success, error, and batch responsestest_jsonrpc_error_codes-- map MCP error codes to ToolErrortest_tool_list_discovery-- parse tools/list responsetest_tool_call_roundtrip-- serialize call, parse result
src/tools/wasm/storage.rs -- 17.9% -> 95% (+228 lines)
WASM tool persistence (store, load, delete, list).
Tests to write:
test_wasm_tool_store_roundtrip-- store and retrieve tool binary + metadatatest_wasm_tool_delete-- remove tool and verify gonetest_wasm_tool_list_filtering-- filter by name, capabilitytest_wasm_tool_update_metadata-- update without re-uploading binary
src/tools/wasm/wrapper.rs -- 56.6% -> 95% (+385 lines)
Tool trait wrapper for WASM modules.
Tests to write:
test_wasm_param_marshalling-- JSON params to WASM component model typestest_wasm_output_conversion-- WASM return values to ToolOutputtest_wasm_error_propagation-- WASM traps to ToolErrortest_wasm_fuel_exhaustion-- verify fuel limit enforcementtest_wasm_memory_limit-- verify memory ceiling
src/tools/wasm/loader.rs -- 62.4% -> 95% (+156 lines)
WASM tool discovery from filesystem.
Tests to write:
test_loader_scan_directory-- find .wasm files with capabilities.jsontest_loader_skip_invalid-- skip files without valid WIT exportstest_loader_cache_invalidation-- reload when file changes
src/tools/builtin/job.rs -- 64.6% -> 95% (+308 lines)
Job management tools (CreateJob, ListJobs, JobStatus, CancelJob).
Tests to write:
test_create_job_params-- validate required/optional parameterstest_list_jobs_formatting-- verify output structuretest_job_status_transitions-- query status at each statetest_cancel_job_running-- cancel an in-progress jobtest_cancel_job_completed-- error on already-completed job
src/secrets/store.rs -- 48.1% -> 95% (+145 lines)
Encrypted secret storage.
Tests to write:
test_secret_store_roundtrip-- store encrypted, retrieve decryptedtest_secret_update-- overwrite existing secrettest_secret_delete-- remove and verify inaccessibletest_secret_list_redacted-- list shows names but not values
src/llm/session.rs -- 41.4% -> 95% (+221 lines)
Session token management with auto-renewal.
Tests to write:
test_session_token_parsing-- parsesess_xxxformattest_session_expiry_detection-- detect expired tokenstest_session_auto_renewal-- trigger renewal before expirytest_session_concurrent_renewal-- only one renewal in flight
src/llm/nearai_chat.rs -- 60.2% -> 95% (+297 lines)
NEAR AI Chat Completions provider.
Tests to write:
test_nearai_request_building-- correct endpoint, headers, bodytest_nearai_response_parsing-- parse streaming and non-streaming responsestest_nearai_tool_message_flattening-- tool messages flattened to texttest_nearai_auth_modes-- session token vs API key authtest_nearai_error_handling-- rate limits, auth failures, server errors
src/llm/mod.rs -- 53.7% -> 95% (+112 lines)
Provider factory and backend selection.
Tests to write:
test_provider_factory_nearai-- select NEAR AI from configtest_provider_factory_openai-- select OpenAI from configtest_provider_factory_ollama-- select Ollama from configtest_provider_factory_invalid-- error on unknown backend
src/llm/reasoning.rs -- 81.2% -> 95% (+160 lines)
Planning, tool selection, evaluation logic.
Tests to write:
test_reasoning_step_parsing-- parse planning steps from LLM outputtest_tool_selection_scoring-- rank tools by relevancetest_evaluation_rubric-- score completions against criteriatest_reasoning_with_no_tools-- handle tool-less responses
src/db/postgres.rs -- 0% -> 95% (+157 lines)
PostgreSQL backend delegation to Store + Repository.
Tests to write:
test_postgres_backend_delegates-- verify delegation pattern (trait-level)test_postgres_connection_config-- TLS, pool size, timeout parsing
src/workspace/mod.rs -- 75.9% -> 95% (+109 lines)
Memory operations (write, read, search, tree).
Tests to write:
test_workspace_write_read-- write document, read it backtest_workspace_search_hybrid-- FTS + vector search via RRFtest_workspace_tree-- directory listing of memory filesystemtest_workspace_overwrite-- update existing document
src/workspace/embeddings.rs -- 35.1% -> 95% (~100 lines)
Embedding provider abstraction.
Tests to write:
test_embedding_dimension_handling-- verify dimension configtest_embedding_batch_processing-- batch multiple chunkstest_embedding_provider_fallback-- graceful degradation when unavailable
Tier 2 -- Trace Tests (~7,000 lines)
End-to-end tests that exercise the agent loop, worker, scheduler, and dispatcher
by replaying LLM traces through TestRig (see tests/support/test_rig.rs). Each
trace test covers multiple modules simultaneously, making them high-leverage.
Each trace test needs:
- A JSON fixture in
tests/fixtures/llm_traces/ - A test file in
tests/usingTestRigBuilder
Trace: Thread Operations
Covers: agent/thread_ops.rs (+710 lines)
Test thread creation, listing, switching, and deletion via trace replay.
Fixture: thread_operations.json
Tests:
test_thread_create_and_switch-- create thread, switch to it, verify contexttest_thread_list-- list all threads, verify metadatatest_thread_delete-- delete thread, verify removaltest_thread_switch_nonexistent-- error handling for missing thread
Trace: Agent Commands
Covers: agent/commands.rs (+557 lines)
Test slash commands through the agent loop.
Fixture: agent_commands.json
Tests:
test_command_help-- /help returns command listtest_command_clear-- /clear resets conversationtest_command_compact-- /compact triggers summarizationtest_command_undo_redo-- /undo then /redo restores statetest_command_status-- /status shows agent state
Trace: Worker Multi-Turn Execution
Covers: worker/job.rs (+413 lines), agent/agent_loop.rs (+207 lines)
Test multi-turn tool calling, error recovery, and completion flows.
Fixture: worker_multi_turn.json
Tests:
test_worker_sequential_tools-- call tool A, then tool B based on A's resulttest_worker_tool_error_recovery-- tool fails, agent retries or adaptstest_worker_max_turns-- verify turn limit enforcement
Trace: Scheduler Parallel Jobs
Covers: agent/scheduler.rs (+235 lines)
Test parallel job dispatch and completion tracking.
Fixture: scheduler_parallel.json
Tests:
test_scheduler_parallel_dispatch-- dispatch 3 jobs, all completetest_scheduler_job_dependency-- job B waits for job Atest_scheduler_stuck_detection-- detect and recover stuck job
Trace: Dispatcher Skill Selection
Covers: agent/dispatcher.rs (+153 lines)
Test skill-aware routing and tool attenuation.
Fixture: dispatcher_skills.json
Tests:
test_dispatcher_skill_match-- match message to skill, inject prompttest_dispatcher_tool_attenuation-- installed skill loses dangerous toolstest_dispatcher_no_skill-- fallback when no skill matches
Trace: Routine Execution
Covers: agent/routine_engine.rs (~80 lines), agent/routine.rs (~40 lines)
Test cron tick and event-triggered routine execution.
Fixture: routine_execution.json
Tests:
test_routine_cron_trigger-- routine fires on scheduletest_routine_event_trigger-- routine fires on matching eventtest_routine_guardrails-- routine respects policy constraints
Trace: Compaction and Context Pressure
Covers: agent/compaction.rs (~50 lines), agent/context_monitor.rs (~30 lines)
Test turn summarization and memory pressure detection.
Fixture: compaction_flow.json
Tests:
test_compaction_triggers_at_threshold-- summarize when context exceeds limittest_compaction_preserves_recent-- keep recent turns intacttest_context_pressure_warning-- emit warning at high usage
Trace: Job Tool Coverage
Covers: tools/builtin/job.rs (+308 lines), tools/builtin/skill_tools.rs (+110 lines)
Test job and skill management tools through agent execution.
Fixture: job_and_skill_tools.json
Tests:
test_create_and_list_jobs-- create job, list shows ittest_job_status_query-- query status of running jobtest_skill_list_and_search-- list local skills, search registry
Trace: Memory Tools
Covers: tools/builtin/memory.rs (~20 lines), workspace/ (+109 lines)
Test memory operations through agent tool calls.
Fixture: memory_tools.json
Tests:
test_memory_write_and_search-- write doc, search finds ittest_memory_read_by_path-- read specific documenttest_memory_tree-- list memory filesystem structure
Trace: Extension Management
Covers: tools/builtin/extension_tools.rs (~40 lines)
Test extension lifecycle via agent tool calls.
Fixture: extension_management.json
Tests:
test_extension_install_via_tool-- agent installs an extensiontest_extension_auth_via_tool-- agent configures authtest_extension_activate_via_tool-- agent activates extension
Trace: Self-Repair
Covers: agent/self_repair.rs (~40 lines)
Test stuck job detection and recovery.
Fixture: self_repair.json
Tests:
test_stuck_job_detected-- job stuck for > threshold triggers repairtest_stuck_job_recovered-- recovery restarts job successfullytest_stuck_job_fails_permanently-- recovery fails, job marked failed
Trace: Heartbeat
Covers: agent/heartbeat.rs (+80 lines)
Test periodic proactive execution.
Fixture: heartbeat.json
Tests:
test_heartbeat_periodic_fire-- heartbeat triggers at intervaltest_heartbeat_reads_checklist-- reads HEARTBEAT.md, processes itemstest_heartbeat_notification-- sends notification on findings
Tier 3 -- Web/Channel Handler Tests (~4,500 lines)
Test HTTP handlers and SSE/WS endpoints using axum_test or
tower::ServiceExt::oneshot with a real router and in-memory database.
src/channels/web/server.rs -- 50% -> 95% (+893 lines)
The single biggest web gap. 40+ API endpoints.
Tests to write:
test_api_health-- GET /health returns 200test_api_chat_submit-- POST /api/chat sends messagetest_api_jobs_list-- GET /api/jobs returns job listtest_api_jobs_create-- POST /api/jobs creates jobtest_api_routines_crud-- full CRUD cycle for routinestest_api_settings_get_set-- GET/PUT settingstest_api_memory_search-- POST /api/memory/searchtest_api_extensions_list-- GET /api/extensionstest_api_skills_list-- GET /api/skillstest_api_sse_connect-- SSE stream connects and receives eventstest_api_auth_required-- endpoints reject missing/bad tokenstest_api_cors_headers-- verify CORS configuration
src/channels/web/handlers/chat.rs -- 26.1% -> 95% (+388 lines)
Chat message submission and SSE streaming.
Tests to write:
test_chat_submit_message-- submit message, receive responsetest_chat_sse_stream-- verify SSE event formattest_chat_thread_context-- messages scoped to threadtest_chat_invalid_payload-- reject malformed requests
src/channels/web/handlers/jobs.rs -- 11.1% -> 95% (+430 lines)
Job CRUD endpoints.
Tests to write:
test_jobs_list_empty-- empty list returns []test_jobs_create_and_get-- create, then GET by IDtest_jobs_cancel-- cancel running jobtest_jobs_filter_by_status-- filter by pending/running/completedtest_jobs_pagination-- limit/offset parameters
src/channels/web/handlers/routines.rs -- 0% -> 95% (+236 lines)
Routine CRUD endpoints.
Tests to write:
test_routines_create-- POST creates routinetest_routines_list-- GET lists all routinestest_routines_update-- PUT updates routine configtest_routines_delete-- DELETE removes routinetest_routines_history-- GET history for a routine
src/channels/web/handlers/extensions.rs -- 0% -> 95% (+129 lines)
Extension management endpoints.
Tests to write:
test_extensions_list-- list installed extensionstest_extensions_install-- install from manifest URLtest_extensions_activate-- activate/deactivate toggletest_extensions_remove-- remove installed extension
src/channels/web/handlers/memory.rs -- 0% -> 95% (+110 lines)
Memory/workspace endpoints.
Tests to write:
test_memory_search-- search returns ranked resultstest_memory_write-- write a documenttest_memory_read-- read by pathtest_memory_tree-- tree returns filesystem structure
src/channels/web/handlers/settings.rs -- 0% -> 95% (+103 lines)
Settings endpoints.
Tests to write:
test_settings_get-- retrieve current settingstest_settings_update-- update individual settingtest_settings_validation-- reject invalid setting values
src/channels/web/handlers/static_files.rs -- 0% -> 95% (+97 lines)
Static file serving.
Tests to write:
test_static_index_html-- GET / serves index.htmltest_static_css_js-- serve CSS/JS with correct content typestest_static_404-- missing file returns 404
src/channels/wasm/wrapper.rs -- 58.2% -> 95% (+822 lines)
WASM channel wrapper (message routing, lifecycle).
Tests to write:
test_wasm_channel_start-- initialize WASM channel moduletest_wasm_channel_message_routing-- route incoming message to WASMtest_wasm_channel_response-- return WASM response to callertest_wasm_channel_error_handling-- handle WASM trap gracefullytest_wasm_channel_lifecycle-- start, process, shutdown
src/channels/wasm/loader.rs -- 38.1% -> 95% (+141 lines)
WASM channel discovery.
Tests to write:
test_channel_loader_scan-- find channel WASM modulestest_channel_loader_validation-- reject invalid modulestest_channel_loader_manifest-- parse channel capabilities
src/channels/wasm/storage.rs -- 0% -> 95% (+172 lines)
WASM channel state persistence.
Tests to write:
test_channel_storage_save_load-- persist and restore channel statetest_channel_storage_isolation-- per-channel state isolationtest_channel_storage_cleanup-- remove state on channel uninstall
src/channels/signal.rs -- 74% -> 95% (+381 lines)
Signal protocol channel.
Tests to write:
test_signal_message_send-- send encrypted messagetest_signal_message_receive-- decrypt incoming messagetest_signal_attachment_handling-- handle media attachmentstest_signal_group_message-- group chat routingtest_signal_error_handling-- handle connection failures
src/channels/repl.rs -- 0% -> 95% (+221 lines)
Simple REPL channel.
Tests to write:
test_repl_input_parsing-- parse user input linestest_repl_output_formatting-- format agent responsestest_repl_multiline-- handle multi-line inputtest_repl_special_commands-- handle /quit, /help
Tier 4 -- CLI Tests (~2,100 lines)
CLI subcommands can be tested by invoking clap-parsed command structs directly or by calling the handler functions with constructed arguments.
src/cli/tool.rs -- 2.9% -> 95% (+697 lines)
Tool CLI (install, list, remove, build).
Tests to write:
test_cli_tool_list-- list installed toolstest_cli_tool_install_local-- install from local .wasm filetest_cli_tool_install_registry-- install from registrytest_cli_tool_remove-- remove installed tooltest_cli_tool_build-- scaffold and build tool projecttest_cli_tool_info-- display tool details
src/cli/mcp.rs -- 0.9% -> 95% (+302 lines)
MCP server management CLI.
Tests to write:
test_cli_mcp_list-- list configured MCP serverstest_cli_mcp_add-- add MCP server configtest_cli_mcp_remove-- remove MCP server configtest_cli_mcp_tools-- list tools from MCP servertest_cli_mcp_test_connection-- verify MCP server reachable
src/cli/oauth_defaults.rs -- 54.1% -> 95% (+298 lines)
OAuth default configurations.
Tests to write:
test_oauth_defaults_loading-- load default OAuth configstest_oauth_url_construction-- build auth/token URLstest_oauth_scope_merging-- merge requested scopes with defaultstest_oauth_provider_lookup-- lookup by provider name
src/cli/registry.rs -- 0% -> 95% (+168 lines)
Registry CLI commands.
Tests to write:
test_cli_registry_search-- search for packagestest_cli_registry_install-- install package from registrytest_cli_registry_info-- display package details
src/cli/status.rs -- 0% -> 95% (+142 lines)
Status display commands.
Tests to write:
test_cli_status_gathering-- collect system status infotest_cli_status_formatting-- render status outputtest_cli_status_components-- check individual components
src/cli/memory.rs -- 15.5% -> 95% (+138 lines)
Memory CLI subcommands.
Tests to write:
test_cli_memory_search-- search workspace from CLItest_cli_memory_write-- write document from CLItest_cli_memory_read-- read document from CLItest_cli_memory_tree-- display memory tree
src/cli/doctor.rs -- 28.7% -> 95% (+115 lines)
Diagnostic checks.
Tests to write:
test_doctor_check_database-- verify DB connectivity checktest_doctor_check_llm-- verify LLM provider checktest_doctor_check_tools-- verify tool availability checktest_doctor_report_format-- verify output format
src/cli/config.rs -- 36.5% -> 95% (~100 lines)
Config CLI subcommands.
Tests to write:
test_cli_config_get-- read config valuetest_cli_config_set-- write config valuetest_cli_config_list-- list all config keystest_cli_config_reset-- reset to defaults
Tier 5 -- Setup/Infra Tests (~2,400 lines)
Hardest to test: interactive wizards, Docker, process spawning. Strategy: extract pure logic into testable functions, test the interactive parts by injecting mock input.
src/setup/wizard.rs -- 16.8% -> 95% (+1,681 lines)
7-step interactive onboarding wizard. Refactor to extract validation functions, step logic, and config generation into testable units.
Tests to write:
test_wizard_step_validation-- each step validates input correctlytest_wizard_config_generation-- generate config from wizard answerstest_wizard_default_values-- verify sensible defaultstest_wizard_skip_completed-- skip already-configured stepstest_wizard_llm_backend_selection-- provider-specific config pathstest_wizard_channel_setup-- channel configuration logic
src/setup/channels.rs -- 7.6% -> 95% (+563 lines)
Channel setup helpers.
Tests to write:
test_channel_setup_defaults-- default channel configurationtest_channel_setup_validation-- reject invalid channel configstest_channel_setup_telegram-- Telegram-specific setup logictest_channel_setup_signal-- Signal-specific setup logictest_channel_setup_webhook-- webhook URL validation
src/setup/prompts.rs -- 24.8% -> 95% (+147 lines)
Terminal prompt utilities.
Tests to write:
test_prompt_select-- selection from listtest_prompt_confirm-- yes/no confirmationtest_prompt_secret-- masked inputtest_prompt_validation-- input validation rules
src/sandbox/container.rs -- 22.1% -> 95% (+296 lines)
Docker container lifecycle. Test command construction without actual Docker.
Tests to write:
test_container_config_to_docker_args-- generate correct docker run argstest_container_volume_mounts-- workspace mount configurationtest_container_env_scrubbing-- sensitive env vars removedtest_container_resource_limits-- CPU/memory limit argstest_container_network_config-- proxy network setup
src/sandbox/manager.rs -- 59% -> 95% (+114 lines)
Sandbox orchestration.
Tests to write:
test_sandbox_policy_enforcement-- policy to container config mappingtest_sandbox_cleanup-- cleanup on job completiontest_sandbox_concurrent_limit-- enforce max concurrent containers
src/sandbox/proxy/http.rs -- 37.5% -> 95% (+176 lines)
HTTP proxy for container network access.
Tests to write:
test_proxy_allowlist_enforcement-- block disallowed domainstest_proxy_credential_injection-- inject auth headerstest_proxy_connect_tunnel-- HTTPS CONNECT method handlingtest_proxy_logging-- request/response logging
src/worker/container.rs -- 5.7% -> 95% (+312 lines)
Worker execution loop (runs inside containers).
Tests to write:
test_worker_tool_dispatch-- dispatch tool call, return resulttest_worker_llm_interaction-- send prompt, receive responsetest_worker_turn_limit-- enforce max turnstest_worker_error_propagation-- tool error surfaces to agent
src/worker/claude_bridge.rs -- 60.7% -> 95% (+215 lines)
Claude CLI bridge.
Tests to write:
test_claude_command_construction-- build claude CLI commandtest_claude_output_parsing-- parse claude CLI JSON outputtest_claude_error_handling-- handle CLI crashes gracefullytest_claude_config_injection-- inject config dir and model
src/worker/api.rs -- 19.8% -> 95% (+194 lines)
Worker HTTP client to orchestrator.
Tests to write:
test_worker_api_request_building-- correct endpoint URLs and headerstest_worker_api_response_parsing-- parse orchestrator responsestest_worker_api_auth_token-- bearer token injectiontest_worker_api_retry-- retry on transient failures
src/main.rs -- 29.4% -> 95% (+485 lines)
Entry point and startup. Extract startup logic into testable functions.
Tests to write:
test_cli_arg_parsing-- verify clap argument parsingtest_startup_config_loading-- config from env + filetest_startup_channel_selection-- select channels from configtest_startup_feature_flags-- feature-gated code paths
Tier 6 -- Remaining Files to 95% (~2,000 lines)
Smaller files that each need a handful of additional tests.
| File | Lines Needed | Test Focus |
|---|---|---|
src/tools/builtin/skill_tools.rs |
110 | skill_list, skill_search, skill_install, skill_remove |
src/hooks/bundled.rs |
115 | bundled hook execution, hook discovery |
src/registry/installer.rs |
272 | package download, verification, installation |
src/registry/artifacts.rs |
72 | artifact packaging, checksums |
src/orchestrator/job_manager.rs |
249 | container lifecycle, job routing |
src/orchestrator/api.rs |
125 | LLM proxy, event dispatch endpoints |
src/app.rs |
137 | AppBuilder configuration, startup sequence |
src/service.rs |
120 | service lifecycle, signal handling |
src/config/channels.rs |
55 | channel config parsing |
src/config/sandbox.rs |
61 | sandbox config parsing |
src/config/tunnel.rs |
43 | tunnel config parsing |
src/config/mod.rs |
63 | config merging, env override |
src/config/database.rs |
38 | database URL parsing |
src/evaluation/success.rs |
34 | success evaluator logic |
src/evaluation/metrics.rs |
40 | metrics collection |
src/context/manager.rs |
57 | concurrent job context isolation |
src/context/memory.rs |
36 | action recording, conversation memory |
Execution Priority
Maximize coverage gain per unit of effort:
| Order | Category | Lines Gained | Effort |
|---|---|---|---|
| 1 | Trace tests (Tier 2) | ~7,000 | Medium (high leverage, each test covers many modules) |
| 2 | Unit tests for 0% files (Tier 1 subset) | ~3,500 | Low (pure logic, no infrastructure) |
| 3 | Web handler tests (Tier 3) | ~4,500 | Medium (axum_test + in-memory DB) |
| 4 | Extension/MCP/WASM unit tests (Tier 1 remainder) | ~3,500 | Medium |
| 5 | CLI subcommand tests (Tier 4) | ~2,100 | Low-Medium |
| 6 | Setup wizard extraction + tests (Tier 5) | ~2,400 | High (requires refactoring) |
| 7 | LLM provider tests (Tier 1 subset) | ~800 | Medium |
| 8 | Remaining small files (Tier 6) | ~2,000 | Low |
Notes
- All trace tests require
--features libsqland useTestRigBuilderfromtests/support/ - Web handler tests can use
axum::testhelpers or build the router directly - CLI tests should call handler functions directly, not shell out to the binary
- Setup wizard tests require extracting pure logic from interactive prompts first
- Sandbox/container tests should verify command construction, not run Docker
- Worker tests can use
TraceLlmfor the LLM provider, same as trace tests