checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-04-01 09:38:59 -04:00
parent 27b6328064
commit a316402acb
5 changed files with 475 additions and 83 deletions

View file

@ -1,6 +1,6 @@
{
"timestamp": "2026-04-01T13:36:36.459530Z",
"current_node": "simplify_gpt",
"timestamp": "2026-04-01T13:38:59.785184Z",
"current_node": "verify",
"completed_nodes": [
"start",
"toolchain",
@ -8,53 +8,89 @@
"preflight_lint",
"implement",
"simplify_opus",
"simplify_gpt"
"simplify_gpt",
"verify"
],
"node_retries": {},
"context_values": {
"response.simplify_opus": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function took `readable` and `writable` as separate boolean params but also called `path.is_dir()` internally — creating redundant filesystem calls (3 syscalls when 2 suffice), parameter sprawl, and an inconsistent state space where callers could pass impossible combinations (e.g. `readable: true` for a non-existent path).\n - **Fix**: Changed signature from `check_storage_dir(path, readable, writable)` to `check_storage_dir(path)`. The function now probes `read_dir()` and `tempfile_in()` internally, deriving existence from the `read_dir` result. Removed the two probe lines from `run_doctor`.\n\n2. **Deleted duplicate integration test** (`tests/it/cmd/doctor.rs`):\n - **Problem**: `storage_dir_shown_in_output` had character-for-character identical expected output to `dry_run_flag`. Since `dry_run_flag`'s snapshot already includes the `[✓] Storage directory ([STORAGE_DIR])` line, the duplicate test added only CI cost.\n - **Fix**: Removed `storage_dir_shown_in_output`. The error path is still covered by `storage_dir_missing_shows_error`.\n\n3. **Eliminated redundant `issues` Vec allocation** (`doctor.rs`):\n - **Problem**: A `Vec` was allocated and `join()`ed just to build a string from at most 2 static `&str` values, then appended as a detail that duplicated the `Readable`/`Writable` lines above it.\n - **Fix**: The restructured function naturally eliminates this — when `readable` fails, we return immediately. The `writable` check only happens when `readable` succeeds, so there's no need to build a combined issues string.\n\n4. **Cleaned up misleading details** (`doctor.rs`):\n - **Problem**: When the directory didn't exist, the details showed `Readable: no` and `Writable: no` — but those weren't meaningful (read_dir/tempfile failed because the dir didn't exist, not due to permissions).\n - **Fix**: When `!readable`, only show `Exists: yes/no`. The `Readable` and `Writable` detail lines only appear when the directory exists and was successfully read.\n\n5. **Removed `check_storage_dir_not_writable` unit test** (`doctor.rs`):\n - **Problem**: With the self-contained function, we can't mock the writable state via parameters. Testing a real unwritable directory is fragile and platform-dependent.\n - **Fix**: Removed it. The error path is adequately covered by the `storage_dir_missing_shows_error` integration test and the `check_storage_dir_not_exists` unit test.\n\n6. **Minor: deduplicated `path.display()` and `\"Storage directory\"` literals** — Bound both to local variables (`display`, `name`) used across all return paths.\n\n**Net result: -38 lines (42 added, 80 removed), cleaner API, no redundant filesystem calls, no duplicate tests.**",
"response.simplify_gpt": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.\n- Split storage-dir handling into:\n - `probe_storage_dir(&Path)` for filesystem probing\n - `check_storage_dir(&StorageDirStatus)` for pure result rendering\n- Made the storage-dir check always report all 3 detail lines:\n - `Exists: ...`\n - `Readable: ...`\n - `Writable: ...`\n- Added the missing unit coverage:\n - existing dir probe\n - missing dir probe\n - pass\n - missing dir error\n - not writable error\n- Added the missing focused integration test:\n - `storage_dir_shown_in_output`\n- Kept existing snapshots green.\n\nValidation:\n- `cargo nextest run -p fabro-cli -- doctor` ✅\n- `cargo clippy --workspace -- -D warnings` ✅\n\nNo further issues stood out after cleanup.",
"internal.retry_count.start": 0,
"thread.start.current_node": "toolchain",
"failure_class": "",
"internal.retry_count.toolchain": 0,
"thread.simplify_opus.current_node": "simplify_gpt",
"failure_signature": "",
"thread.preflight_lint.current_node": "implement",
"internal.retry_count.preflight_compile": 0,
"internal.fidelity": "compact",
"internal.retry_count.preflight_lint": 0,
"internal.retry_count.simplify_opus": 0,
"current.preamble": "Goal: # Add storage directory check to `fabro doctor`\n\n## Context\n\n`fabro doctor` validates the installation but doesn't check that the storage directory (where runs, store data, etc. live) exists and is usable. Adding this check surfaces misconfiguration early — e.g. a `storage_dir` override pointing to a nonexistent or read-only path.\n\n## Plan\n\n### 1. Add `check_storage_dir` pure function in `doctor.rs`\n\n**File:** `lib/crates/fabro-cli/src/commands/doctor.rs`\n\nAdd a function like the existing `check_config`:\n\n```rust\nfn check_storage_dir(path: &Path, readable: bool, writable: bool) -> CheckResult\n```\n\n- **Summary always shows the resolved path** (e.g. `/Users/you/.fabro`) — same pattern as `check_config` which puts the path in `summary`.\n- **Pass** — dir exists, readable, writable.\n- **Error** — dir doesn't exist, or not readable, or not writable. Remediation: create it or fix permissions.\n- Details (verbose): existence, read, write status as individual lines.\n\n### 2. Gather state in `run_doctor`\n\nBefore the pure-checks section, resolve the storage dir and probe it:\n\n```rust\nlet storage_dir = cli_settings.storage_dir();\nlet exists = storage_dir.is_dir();\nlet readable = std::fs::read_dir(&storage_dir).is_ok();\nlet writable = tempfile::tempfile_in(&storage_dir).is_ok(); // or write+remove a temp file\n```\n\nUse `std::fs` directly — no async/live probe needed for local filesystem checks.\n\n### 3. Add to \"Required\" section\n\nInsert `check_storage_dir` result into the \"Required\" section, after the \"Configuration\" check and before \"LLM providers\" — storage is fundamental.\n\n### 4. Add unit tests\n\nFollow the existing test pattern (pure function tests with synthetic inputs). Cover:\n- Dir exists + readable + writable → Pass\n- Dir doesn't exist → Error\n- Dir exists but not writable → Error\n\nUse a `tempdir` for real filesystem assertions in a couple of tests.\n\n### 5. Add integration tests in `it/cmd/doctor.rs`\n\n**File:** `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` (existing, has 4 tests)\n\nAdd 2 tests using the existing `test_context!()` + `fabro_snapshot!` pattern:\n\n- **`storage_dir_shown_in_output`** — `TestContext` already creates a temp `storage_dir` and sets `FABRO_STORAGE_DIR`. Run `doctor --dry-run`, snapshot-assert that \"Storage directory\" line appears with the path in the summary.\n- **`storage_dir_missing_shows_error`** — Override `FABRO_STORAGE_DIR` to a nonexistent path via `.env(\"FABRO_STORAGE_DIR\", \"/tmp/nonexistent-fabro-xyz\")`. Run `doctor --dry-run`, snapshot-assert that the check shows error status with the path and remediation text.\n\nBoth tests use `--dry-run` to skip live probes and `fabro_snapshot!` for inline snapshot assertions. Add a filter to normalize the temp dir path (e.g. `[STORAGE_DIR]`).\n\n## Files to modify\n\n- `lib/crates/fabro-cli/src/commands/doctor.rs` — new check function + gather state + wire into section + unit tests\n- `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` — 2 new integration tests\n\n## Verification\n\n- `cargo nextest run -p fabro-cli -- doctor` — unit + integration tests pass\n- `cargo clippy --workspace -- -D warnings` — no lint issues\n- `fabro doctor` — shows new \"Storage directory\" check with the resolved path\n- `fabro doctor -v` — shows detail lines for existence/read/write\n- `FABRO_STORAGE_DIR=/nonexistent fabro doctor` — shows error for missing dir\n\n\n## Completed stages\n- **toolchain**: success\n - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **preflight_lint**: success\n - Script: `cargo clippy -q --workspace -- -D warnings 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **implement**: success\n - Model: claude-opus-4-6, 74.4k tokens in / 10.7k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs, /home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs\n- **simplify_opus**: success\n - Model: claude-opus-4-6, 48.3k tokens in / 13.2k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs, /home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs\n",
"thread.preflight_compile.current_node": "preflight_lint",
"last_response": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.",
"graph.model_stylesheet": "\n * { model: claude-opus-4-6; }\n ",
"internal.run_id": "01KN4JD6GJTC3PHC8G80EA950B",
"outcome": "success",
"command.output": "",
"thread.implement.current_node": "simplify_opus",
"thread.toolchain.current_node": "preflight_compile",
"internal.retry_count.implement": 0,
"graph.goal": "# Add storage directory check to `fabro doctor`\n\n## Context\n\n`fabro doctor` validates the installation but doesn't check that the storage directory (where runs, store data, etc. live) exists and is usable. Adding this check surfaces misconfiguration early — e.g. a `storage_dir` override pointing to a nonexistent or read-only path.\n\n## Plan\n\n### 1. Add `check_storage_dir` pure function in `doctor.rs`\n\n**File:** `lib/crates/fabro-cli/src/commands/doctor.rs`\n\nAdd a function like the existing `check_config`:\n\n```rust\nfn check_storage_dir(path: &Path, readable: bool, writable: bool) -> CheckResult\n```\n\n- **Summary always shows the resolved path** (e.g. `/Users/you/.fabro`) — same pattern as `check_config` which puts the path in `summary`.\n- **Pass** — dir exists, readable, writable.\n- **Error** — dir doesn't exist, or not readable, or not writable. Remediation: create it or fix permissions.\n- Details (verbose): existence, read, write status as individual lines.\n\n### 2. Gather state in `run_doctor`\n\nBefore the pure-checks section, resolve the storage dir and probe it:\n\n```rust\nlet storage_dir = cli_settings.storage_dir();\nlet exists = storage_dir.is_dir();\nlet readable = std::fs::read_dir(&storage_dir).is_ok();\nlet writable = tempfile::tempfile_in(&storage_dir).is_ok(); // or write+remove a temp file\n```\n\nUse `std::fs` directly — no async/live probe needed for local filesystem checks.\n\n### 3. Add to \"Required\" section\n\nInsert `check_storage_dir` result into the \"Required\" section, after the \"Configuration\" check and before \"LLM providers\" — storage is fundamental.\n\n### 4. Add unit tests\n\nFollow the existing test pattern (pure function tests with synthetic inputs). Cover:\n- Dir exists + readable + writable → Pass\n- Dir doesn't exist → Error\n- Dir exists but not writable → Error\n\nUse a `tempdir` for real filesystem assertions in a couple of tests.\n\n### 5. Add integration tests in `it/cmd/doctor.rs`\n\n**File:** `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` (existing, has 4 tests)\n\nAdd 2 tests using the existing `test_context!()` + `fabro_snapshot!` pattern:\n\n- **`storage_dir_shown_in_output`** — `TestContext` already creates a temp `storage_dir` and sets `FABRO_STORAGE_DIR`. Run `doctor --dry-run`, snapshot-assert that \"Storage directory\" line appears with the path in the summary.\n- **`storage_dir_missing_shows_error`** — Override `FABRO_STORAGE_DIR` to a nonexistent path via `.env(\"FABRO_STORAGE_DIR\", \"/tmp/nonexistent-fabro-xyz\")`. Run `doctor --dry-run`, snapshot-assert that the check shows error status with the path and remediation text.\n\nBoth tests use `--dry-run` to skip live probes and `fabro_snapshot!` for inline snapshot assertions. Add a filter to normalize the temp dir path (e.g. `[STORAGE_DIR]`).\n\n## Files to modify\n\n- `lib/crates/fabro-cli/src/commands/doctor.rs` — new check function + gather state + wire into section + unit tests\n- `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` — 2 new integration tests\n\n## Verification\n\n- `cargo nextest run -p fabro-cli -- doctor` — unit + integration tests pass\n- `cargo clippy --workspace -- -D warnings` — no lint issues\n- `fabro doctor` — shows new \"Storage directory\" check with the resolved path\n- `fabro doctor -v` — shows detail lines for existence/read/write\n- `FABRO_STORAGE_DIR=/nonexistent fabro doctor` — shows error for missing dir\n",
"internal.retry_count.simplify_gpt": 0,
"graph.rankdir": "LR",
"response.implement": "All clean. Here's a summary of all changes made:\n\n## Summary of Changes\n\n### Files modified\n\n**1. `lib/crates/fabro-cli/src/commands/doctor.rs`**\n\n- **Import additions**: Added `Path` to `std::path::{Path, PathBuf}` import and added `use fabro_config::FabroSettingsExt;` for the `storage_dir()` method on `FabroSettings`.\n\n- **New pure function `check_storage_dir`**: Added between `check_config` and `check_llm_providers`. Takes `path: &Path`, `readable: bool`, `writable: bool` and returns a `CheckResult`:\n - **Pass**: directory exists, readable, and writable. Summary shows the resolved path.\n - **Error (not exists)**: remediation suggests `mkdir -p <path>`.\n - **Error (permission issues)**: remediation suggests fixing permissions.\n - Verbose details always show existence/read/write status lines.\n\n- **State gathering in `run_doctor`**: After config path checks, resolves the storage directory using `globals.storage_dir` (CLI arg / env var) with fallback to `cli_settings.storage_dir()`. Probes readability via `std::fs::read_dir()` and writability via `tempfile::tempfile_in()`.\n\n- **Wired into \"Required\" section**: Inserted `check_storage_dir()` result after \"Configuration\" and before \"LLM providers\".\n\n- **3 unit tests added** in `mod tests`:\n - `check_storage_dir_pass` — real tempdir → Pass\n - `check_storage_dir_not_exists` — nonexistent path → Error with `mkdir -p` remediation\n - `check_storage_dir_not_writable` — exists but not writable → Error with \"Fix permissions\" remediation\n\n**2. `lib/crates/fabro-cli/tests/it/cmd/doctor.rs`**\n\n- **Updated `dry_run_flag` snapshot**: Added the new `[✓] Storage directory ([STORAGE_DIR])` line.\n\n- **2 new integration tests added**:\n - `storage_dir_shown_in_output` — runs `doctor --dry-run` and verifies \"Storage directory\" appears with `[STORAGE_DIR]` in the summary.\n - `storage_dir_missing_shows_error` — overrides `FABRO_STORAGE_DIR` to a nonexistent path, verifies `[✗]` error status and `mkdir -p` remediation text.",
"internal.thread_id": "simplify_opus",
"current_node": "simplify_gpt",
"command.stderr": "",
"thread.start.current_node": "toolchain",
"thread.preflight_compile.current_node": "preflight_lint",
"internal.thread_id": "simplify_gpt",
"current_node": "verify",
"internal.retry_count.simplify_opus": 0,
"last_stage": "simplify_gpt",
"internal.retry_count.toolchain": 0,
"graph.rankdir": "LR",
"thread.simplify_gpt.current_node": "verify",
"internal.retry_count.preflight_lint": 0,
"failure_class": "deterministic",
"graph.goal": "# Add storage directory check to `fabro doctor`\n\n## Context\n\n`fabro doctor` validates the installation but doesn't check that the storage directory (where runs, store data, etc. live) exists and is usable. Adding this check surfaces misconfiguration early — e.g. a `storage_dir` override pointing to a nonexistent or read-only path.\n\n## Plan\n\n### 1. Add `check_storage_dir` pure function in `doctor.rs`\n\n**File:** `lib/crates/fabro-cli/src/commands/doctor.rs`\n\nAdd a function like the existing `check_config`:\n\n```rust\nfn check_storage_dir(path: &Path, readable: bool, writable: bool) -> CheckResult\n```\n\n- **Summary always shows the resolved path** (e.g. `/Users/you/.fabro`) — same pattern as `check_config` which puts the path in `summary`.\n- **Pass** — dir exists, readable, writable.\n- **Error** — dir doesn't exist, or not readable, or not writable. Remediation: create it or fix permissions.\n- Details (verbose): existence, read, write status as individual lines.\n\n### 2. Gather state in `run_doctor`\n\nBefore the pure-checks section, resolve the storage dir and probe it:\n\n```rust\nlet storage_dir = cli_settings.storage_dir();\nlet exists = storage_dir.is_dir();\nlet readable = std::fs::read_dir(&storage_dir).is_ok();\nlet writable = tempfile::tempfile_in(&storage_dir).is_ok(); // or write+remove a temp file\n```\n\nUse `std::fs` directly — no async/live probe needed for local filesystem checks.\n\n### 3. Add to \"Required\" section\n\nInsert `check_storage_dir` result into the \"Required\" section, after the \"Configuration\" check and before \"LLM providers\" — storage is fundamental.\n\n### 4. Add unit tests\n\nFollow the existing test pattern (pure function tests with synthetic inputs). Cover:\n- Dir exists + readable + writable → Pass\n- Dir doesn't exist → Error\n- Dir exists but not writable → Error\n\nUse a `tempdir` for real filesystem assertions in a couple of tests.\n\n### 5. Add integration tests in `it/cmd/doctor.rs`\n\n**File:** `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` (existing, has 4 tests)\n\nAdd 2 tests using the existing `test_context!()` + `fabro_snapshot!` pattern:\n\n- **`storage_dir_shown_in_output`** — `TestContext` already creates a temp `storage_dir` and sets `FABRO_STORAGE_DIR`. Run `doctor --dry-run`, snapshot-assert that \"Storage directory\" line appears with the path in the summary.\n- **`storage_dir_missing_shows_error`** — Override `FABRO_STORAGE_DIR` to a nonexistent path via `.env(\"FABRO_STORAGE_DIR\", \"/tmp/nonexistent-fabro-xyz\")`. Run `doctor --dry-run`, snapshot-assert that the check shows error status with the path and remediation text.\n\nBoth tests use `--dry-run` to skip live probes and `fabro_snapshot!` for inline snapshot assertions. Add a filter to normalize the temp dir path (e.g. `[STORAGE_DIR]`).\n\n## Files to modify\n\n- `lib/crates/fabro-cli/src/commands/doctor.rs` — new check function + gather state + wire into section + unit tests\n- `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` — 2 new integration tests\n\n## Verification\n\n- `cargo nextest run -p fabro-cli -- doctor` — unit + integration tests pass\n- `cargo clippy --workspace -- -D warnings` — no lint issues\n- `fabro doctor` — shows new \"Storage directory\" check with the resolved path\n- `fabro doctor -v` — shows detail lines for existence/read/write\n- `FABRO_STORAGE_DIR=/nonexistent fabro doctor` — shows error for missing dir\n",
"internal.run_id": "01KN4JD6GJTC3PHC8G80EA950B",
"internal.node_visit_count": 1,
"last_stage": "simplify_gpt"
"thread.toolchain.current_node": "preflight_compile",
"thread.implement.current_node": "simplify_opus",
"response.implement": "All clean. Here's a summary of all changes made:\n\n## Summary of Changes\n\n### Files modified\n\n**1. `lib/crates/fabro-cli/src/commands/doctor.rs`**\n\n- **Import additions**: Added `Path` to `std::path::{Path, PathBuf}` import and added `use fabro_config::FabroSettingsExt;` for the `storage_dir()` method on `FabroSettings`.\n\n- **New pure function `check_storage_dir`**: Added between `check_config` and `check_llm_providers`. Takes `path: &Path`, `readable: bool`, `writable: bool` and returns a `CheckResult`:\n - **Pass**: directory exists, readable, and writable. Summary shows the resolved path.\n - **Error (not exists)**: remediation suggests `mkdir -p <path>`.\n - **Error (permission issues)**: remediation suggests fixing permissions.\n - Verbose details always show existence/read/write status lines.\n\n- **State gathering in `run_doctor`**: After config path checks, resolves the storage directory using `globals.storage_dir` (CLI arg / env var) with fallback to `cli_settings.storage_dir()`. Probes readability via `std::fs::read_dir()` and writability via `tempfile::tempfile_in()`.\n\n- **Wired into \"Required\" section**: Inserted `check_storage_dir()` result after \"Configuration\" and before \"LLM providers\".\n\n- **3 unit tests added** in `mod tests`:\n - `check_storage_dir_pass` — real tempdir → Pass\n - `check_storage_dir_not_exists` — nonexistent path → Error with `mkdir -p` remediation\n - `check_storage_dir_not_writable` — exists but not writable → Error with \"Fix permissions\" remediation\n\n**2. `lib/crates/fabro-cli/tests/it/cmd/doctor.rs`**\n\n- **Updated `dry_run_flag` snapshot**: Added the new `[✓] Storage directory ([STORAGE_DIR])` line.\n\n- **2 new integration tests added**:\n - `storage_dir_shown_in_output` — runs `doctor --dry-run` and verifies \"Storage directory\" appears with `[STORAGE_DIR]` in the summary.\n - `storage_dir_missing_shows_error` — overrides `FABRO_STORAGE_DIR` to a nonexistent path, verifies `[✗]` error status and `mkdir -p` remediation text.",
"thread.preflight_lint.current_node": "implement",
"graph.model_stylesheet": "\n * { model: claude-opus-4-6; }\n ",
"internal.retry_count.simplify_gpt": 0,
"last_response": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.",
"internal.retry_count.verify": 0,
"response.simplify_opus": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function took `readable` and `writable` as separate boolean params but also called `path.is_dir()` internally — creating redundant filesystem calls (3 syscalls when 2 suffice), parameter sprawl, and an inconsistent state space where callers could pass impossible combinations (e.g. `readable: true` for a non-existent path).\n - **Fix**: Changed signature from `check_storage_dir(path, readable, writable)` to `check_storage_dir(path)`. The function now probes `read_dir()` and `tempfile_in()` internally, deriving existence from the `read_dir` result. Removed the two probe lines from `run_doctor`.\n\n2. **Deleted duplicate integration test** (`tests/it/cmd/doctor.rs`):\n - **Problem**: `storage_dir_shown_in_output` had character-for-character identical expected output to `dry_run_flag`. Since `dry_run_flag`'s snapshot already includes the `[✓] Storage directory ([STORAGE_DIR])` line, the duplicate test added only CI cost.\n - **Fix**: Removed `storage_dir_shown_in_output`. The error path is still covered by `storage_dir_missing_shows_error`.\n\n3. **Eliminated redundant `issues` Vec allocation** (`doctor.rs`):\n - **Problem**: A `Vec` was allocated and `join()`ed just to build a string from at most 2 static `&str` values, then appended as a detail that duplicated the `Readable`/`Writable` lines above it.\n - **Fix**: The restructured function naturally eliminates this — when `readable` fails, we return immediately. The `writable` check only happens when `readable` succeeds, so there's no need to build a combined issues string.\n\n4. **Cleaned up misleading details** (`doctor.rs`):\n - **Problem**: When the directory didn't exist, the details showed `Readable: no` and `Writable: no` — but those weren't meaningful (read_dir/tempfile failed because the dir didn't exist, not due to permissions).\n - **Fix**: When `!readable`, only show `Exists: yes/no`. The `Readable` and `Writable` detail lines only appear when the directory exists and was successfully read.\n\n5. **Removed `check_storage_dir_not_writable` unit test** (`doctor.rs`):\n - **Problem**: With the self-contained function, we can't mock the writable state via parameters. Testing a real unwritable directory is fragile and platform-dependent.\n - **Fix**: Removed it. The error path is adequately covered by the `storage_dir_missing_shows_error` integration test and the `check_storage_dir_not_exists` unit test.\n\n6. **Minor: deduplicated `path.display()` and `\"Storage directory\"` literals** — Bound both to local variables (`display`, `name`) used across all return paths.\n\n**Net result: -38 lines (42 added, 80 removed), cleaner API, no redundant filesystem calls, no duplicate tests.**",
"internal.retry_count.start": 0,
"command.output": "error[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:389:22\n |\n389 | key: Arc::new(decoding_key),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:390:29\n |\n390 | validation: Arc::new(fabro_server::jwt_auth::jwt_validation()),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nFor more information about this error, try `rustc --explain E0433`.\nerror: could not compile `fabro-server` (test \"it\") due to 2 previous errors\nerror: command `/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --quiet` exited with code 101\n",
"failure_signature": "verify|deterministic|script failed with exit code: <n> ## stdout error[e0433]: failed to resolve: use of undeclared type `arc` --> lib/crates/fabro-server/tests/it/api.rs:<n>:<n> | <n> | key: arc::new(decoding_key),| ^^^ use of undeclared type `arc` | help: con",
"internal.retry_count.implement": 0,
"outcome": "fail",
"thread.simplify_opus.current_node": "simplify_gpt",
"current.preamble": "Goal: # Add storage directory check to `fabro doctor`\n\n## Context\n\n`fabro doctor` validates the installation but doesn't check that the storage directory (where runs, store data, etc. live) exists and is usable. Adding this check surfaces misconfiguration early — e.g. a `storage_dir` override pointing to a nonexistent or read-only path.\n\n## Plan\n\n### 1. Add `check_storage_dir` pure function in `doctor.rs`\n\n**File:** `lib/crates/fabro-cli/src/commands/doctor.rs`\n\nAdd a function like the existing `check_config`:\n\n```rust\nfn check_storage_dir(path: &Path, readable: bool, writable: bool) -> CheckResult\n```\n\n- **Summary always shows the resolved path** (e.g. `/Users/you/.fabro`) — same pattern as `check_config` which puts the path in `summary`.\n- **Pass** — dir exists, readable, writable.\n- **Error** — dir doesn't exist, or not readable, or not writable. Remediation: create it or fix permissions.\n- Details (verbose): existence, read, write status as individual lines.\n\n### 2. Gather state in `run_doctor`\n\nBefore the pure-checks section, resolve the storage dir and probe it:\n\n```rust\nlet storage_dir = cli_settings.storage_dir();\nlet exists = storage_dir.is_dir();\nlet readable = std::fs::read_dir(&storage_dir).is_ok();\nlet writable = tempfile::tempfile_in(&storage_dir).is_ok(); // or write+remove a temp file\n```\n\nUse `std::fs` directly — no async/live probe needed for local filesystem checks.\n\n### 3. Add to \"Required\" section\n\nInsert `check_storage_dir` result into the \"Required\" section, after the \"Configuration\" check and before \"LLM providers\" — storage is fundamental.\n\n### 4. Add unit tests\n\nFollow the existing test pattern (pure function tests with synthetic inputs). Cover:\n- Dir exists + readable + writable → Pass\n- Dir doesn't exist → Error\n- Dir exists but not writable → Error\n\nUse a `tempdir` for real filesystem assertions in a couple of tests.\n\n### 5. Add integration tests in `it/cmd/doctor.rs`\n\n**File:** `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` (existing, has 4 tests)\n\nAdd 2 tests using the existing `test_context!()` + `fabro_snapshot!` pattern:\n\n- **`storage_dir_shown_in_output`** — `TestContext` already creates a temp `storage_dir` and sets `FABRO_STORAGE_DIR`. Run `doctor --dry-run`, snapshot-assert that \"Storage directory\" line appears with the path in the summary.\n- **`storage_dir_missing_shows_error`** — Override `FABRO_STORAGE_DIR` to a nonexistent path via `.env(\"FABRO_STORAGE_DIR\", \"/tmp/nonexistent-fabro-xyz\")`. Run `doctor --dry-run`, snapshot-assert that the check shows error status with the path and remediation text.\n\nBoth tests use `--dry-run` to skip live probes and `fabro_snapshot!` for inline snapshot assertions. Add a filter to normalize the temp dir path (e.g. `[STORAGE_DIR]`).\n\n## Files to modify\n\n- `lib/crates/fabro-cli/src/commands/doctor.rs` — new check function + gather state + wire into section + unit tests\n- `lib/crates/fabro-cli/tests/it/cmd/doctor.rs` — 2 new integration tests\n\n## Verification\n\n- `cargo nextest run -p fabro-cli -- doctor` — unit + integration tests pass\n- `cargo clippy --workspace -- -D warnings` — no lint issues\n- `fabro doctor` — shows new \"Storage directory\" check with the resolved path\n- `fabro doctor -v` — shows detail lines for existence/read/write\n- `FABRO_STORAGE_DIR=/nonexistent fabro doctor` — shows error for missing dir\n\n\n## Completed stages\n- **toolchain**: success\n - Script: `command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1`\n - Stdout:\n ```\n cargo 1.94.0 (85eff7c80 2026-01-15)\n ```\n - Stderr: (empty)\n- **preflight_compile**: success\n - Script: `cargo check -q --workspace 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **preflight_lint**: success\n - Script: `cargo clippy -q --workspace -- -D warnings 2>&1`\n - Stdout: (empty)\n - Stderr: (empty)\n- **implement**: success\n - Model: claude-opus-4-6, 74.4k tokens in / 10.7k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs, /home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs\n- **simplify_opus**: success\n - Model: claude-opus-4-6, 48.3k tokens in / 13.2k out\n - Files: /home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs, /home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs\n- **simplify_gpt**: success\n - Model: gpt-5.4, 2.5m tokens in / 26.5k out\n",
"internal.fidelity": "compact",
"internal.retry_count.preflight_compile": 0,
"response.simplify_gpt": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.\n- Split storage-dir handling into:\n - `probe_storage_dir(&Path)` for filesystem probing\n - `check_storage_dir(&StorageDirStatus)` for pure result rendering\n- Made the storage-dir check always report all 3 detail lines:\n - `Exists: ...`\n - `Readable: ...`\n - `Writable: ...`\n- Added the missing unit coverage:\n - existing dir probe\n - missing dir probe\n - pass\n - missing dir error\n - not writable error\n- Added the missing focused integration test:\n - `storage_dir_shown_in_output`\n- Kept existing snapshots green.\n\nValidation:\n- `cargo nextest run -p fabro-cli -- doctor` ✅\n- `cargo clippy --workspace -- -D warnings` ✅\n\nNo further issues stood out after cleanup."
},
"node_outcomes": {
"preflight_lint": {
"status": "success",
"context_updates": {
"command.stderr": "",
"command.output": ""
"command.output": "",
"command.stderr": ""
},
"notes": "Script completed: cargo clippy -q --workspace -- -D warnings 2>&1",
"usage": null
},
"start": {
"status": "success",
"usage": null
},
"verify": {
"status": "fail",
"context_updates": {
"command.output": "error[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:389:22\n |\n389 | key: Arc::new(decoding_key),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:390:29\n |\n390 | validation: Arc::new(fabro_server::jwt_auth::jwt_validation()),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nFor more information about this error, try `rustc --explain E0433`.\nerror: could not compile `fabro-server` (test \"it\") due to 2 previous errors\nerror: command `/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --quiet` exited with code 101\n",
"command.stderr": ""
},
"failure": {
"message": "Script failed with exit code: 101\n\n## stdout\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:389:22\n |\n389 | key: Arc::new(decoding_key),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:390:29\n |\n390 | validation: Arc::new(fabro_server::jwt_auth::jwt_validation()),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nFor more information about this error, try `rustc --explain E0433`.\nerror: could not compile `fabro-server` (test \"it\") due to 2 previous errors\nerror: command `/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --quiet` exited with code 101\n",
"failure_class": "deterministic"
},
"usage": null
},
"simplify_gpt": {
"status": "success",
"context_updates": {
"last_response": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.",
"last_stage": "simplify_gpt",
"response.simplify_gpt": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.\n- Split storage-dir handling into:\n - `probe_storage_dir(&Path)` for filesystem probing\n - `check_storage_dir(&StorageDirStatus)` for pure result rendering\n- Made the storage-dir check always report all 3 detail lines:\n - `Exists: ...`\n - `Readable: ...`\n - `Writable: ...`\n- Added the missing unit coverage:\n - existing dir probe\n - missing dir probe\n - pass\n - missing dir error\n - not writable error\n- Added the missing focused integration test:\n - `storage_dir_shown_in_output`\n- Kept existing snapshots green.\n\nValidation:\n- `cargo nextest run -p fabro-cli -- doctor` ✅\n- `cargo clippy --workspace -- -D warnings` ✅\n\nNo further issues stood out after cleanup."
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"model": "gpt-5.4",
"input_tokens": 2475751,
"output_tokens": 26534,
"cache_read_tokens": 162560,
"reasoning_tokens": 8573,
"cost": 6.5873875
}
},
"toolchain": {
"status": "success",
"context_updates": {
@ -64,31 +100,6 @@
"notes": "Script completed: command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1",
"usage": null
},
"simplify_opus": {
"status": "success",
"context_updates": {
"last_stage": "simplify_opus",
"last_response": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function to",
"response.simplify_opus": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function took `readable` and `writable` as separate boolean params but also called `path.is_dir()` internally — creating redundant filesystem calls (3 syscalls when 2 suffice), parameter sprawl, and an inconsistent state space where callers could pass impossible combinations (e.g. `readable: true` for a non-existent path).\n - **Fix**: Changed signature from `check_storage_dir(path, readable, writable)` to `check_storage_dir(path)`. The function now probes `read_dir()` and `tempfile_in()` internally, deriving existence from the `read_dir` result. Removed the two probe lines from `run_doctor`.\n\n2. **Deleted duplicate integration test** (`tests/it/cmd/doctor.rs`):\n - **Problem**: `storage_dir_shown_in_output` had character-for-character identical expected output to `dry_run_flag`. Since `dry_run_flag`'s snapshot already includes the `[✓] Storage directory ([STORAGE_DIR])` line, the duplicate test added only CI cost.\n - **Fix**: Removed `storage_dir_shown_in_output`. The error path is still covered by `storage_dir_missing_shows_error`.\n\n3. **Eliminated redundant `issues` Vec allocation** (`doctor.rs`):\n - **Problem**: A `Vec` was allocated and `join()`ed just to build a string from at most 2 static `&str` values, then appended as a detail that duplicated the `Readable`/`Writable` lines above it.\n - **Fix**: The restructured function naturally eliminates this — when `readable` fails, we return immediately. The `writable` check only happens when `readable` succeeds, so there's no need to build a combined issues string.\n\n4. **Cleaned up misleading details** (`doctor.rs`):\n - **Problem**: When the directory didn't exist, the details showed `Readable: no` and `Writable: no` — but those weren't meaningful (read_dir/tempfile failed because the dir didn't exist, not due to permissions).\n - **Fix**: When `!readable`, only show `Exists: yes/no`. The `Readable` and `Writable` detail lines only appear when the directory exists and was successfully read.\n\n5. **Removed `check_storage_dir_not_writable` unit test** (`doctor.rs`):\n - **Problem**: With the self-contained function, we can't mock the writable state via parameters. Testing a real unwritable directory is fragile and platform-dependent.\n - **Fix**: Removed it. The error path is adequately covered by the `storage_dir_missing_shows_error` integration test and the `check_storage_dir_not_exists` unit test.\n\n6. **Minor: deduplicated `path.display()` and `\"Storage directory\"` literals** — Bound both to local variables (`display`, `name`) used across all return paths.\n\n**Net result: -38 lines (42 added, 80 removed), cleaner API, no redundant filesystem calls, no duplicate tests.**"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"model": "claude-opus-4-6",
"input_tokens": 48295,
"output_tokens": 13230,
"cache_read_tokens": 827171,
"cache_write_tokens": 52524,
"cost": 1.716675
},
"files_touched": [
"/home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs",
"/home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs"
]
},
"start": {
"status": "success",
"usage": null
},
"implement": {
"status": "success",
"context_updates": {
@ -110,23 +121,6 @@
"/home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs"
]
},
"simplify_gpt": {
"status": "success",
"context_updates": {
"last_stage": "simplify_gpt",
"last_response": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.",
"response.simplify_gpt": "Reviewed and cleaned up the `fabro doctor` storage-dir work.\n\nWhat I fixed:\n- Reused `load_user_settings_with_globals(globals)` so doctor uses the same effective settings resolution as other commands.\n- Split storage-dir handling into:\n - `probe_storage_dir(&Path)` for filesystem probing\n - `check_storage_dir(&StorageDirStatus)` for pure result rendering\n- Made the storage-dir check always report all 3 detail lines:\n - `Exists: ...`\n - `Readable: ...`\n - `Writable: ...`\n- Added the missing unit coverage:\n - existing dir probe\n - missing dir probe\n - pass\n - missing dir error\n - not writable error\n- Added the missing focused integration test:\n - `storage_dir_shown_in_output`\n- Kept existing snapshots green.\n\nValidation:\n- `cargo nextest run -p fabro-cli -- doctor` ✅\n- `cargo clippy --workspace -- -D warnings` ✅\n\nNo further issues stood out after cleanup."
},
"notes": "Stage completed: simplify_gpt",
"usage": {
"model": "gpt-5.4",
"input_tokens": 2475751,
"output_tokens": 26534,
"cache_read_tokens": 162560,
"reasoning_tokens": 8573,
"cost": 6.5873875
}
},
"preflight_compile": {
"status": "success",
"context_updates": {
@ -135,16 +129,41 @@
},
"notes": "Script completed: cargo check -q --workspace 2>&1",
"usage": null
},
"simplify_opus": {
"status": "success",
"context_updates": {
"response.simplify_opus": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function took `readable` and `writable` as separate boolean params but also called `path.is_dir()` internally — creating redundant filesystem calls (3 syscalls when 2 suffice), parameter sprawl, and an inconsistent state space where callers could pass impossible combinations (e.g. `readable: true` for a non-existent path).\n - **Fix**: Changed signature from `check_storage_dir(path, readable, writable)` to `check_storage_dir(path)`. The function now probes `read_dir()` and `tempfile_in()` internally, deriving existence from the `read_dir` result. Removed the two probe lines from `run_doctor`.\n\n2. **Deleted duplicate integration test** (`tests/it/cmd/doctor.rs`):\n - **Problem**: `storage_dir_shown_in_output` had character-for-character identical expected output to `dry_run_flag`. Since `dry_run_flag`'s snapshot already includes the `[✓] Storage directory ([STORAGE_DIR])` line, the duplicate test added only CI cost.\n - **Fix**: Removed `storage_dir_shown_in_output`. The error path is still covered by `storage_dir_missing_shows_error`.\n\n3. **Eliminated redundant `issues` Vec allocation** (`doctor.rs`):\n - **Problem**: A `Vec` was allocated and `join()`ed just to build a string from at most 2 static `&str` values, then appended as a detail that duplicated the `Readable`/`Writable` lines above it.\n - **Fix**: The restructured function naturally eliminates this — when `readable` fails, we return immediately. The `writable` check only happens when `readable` succeeds, so there's no need to build a combined issues string.\n\n4. **Cleaned up misleading details** (`doctor.rs`):\n - **Problem**: When the directory didn't exist, the details showed `Readable: no` and `Writable: no` — but those weren't meaningful (read_dir/tempfile failed because the dir didn't exist, not due to permissions).\n - **Fix**: When `!readable`, only show `Exists: yes/no`. The `Readable` and `Writable` detail lines only appear when the directory exists and was successfully read.\n\n5. **Removed `check_storage_dir_not_writable` unit test** (`doctor.rs`):\n - **Problem**: With the self-contained function, we can't mock the writable state via parameters. Testing a real unwritable directory is fragile and platform-dependent.\n - **Fix**: Removed it. The error path is adequately covered by the `storage_dir_missing_shows_error` integration test and the `check_storage_dir_not_exists` unit test.\n\n6. **Minor: deduplicated `path.display()` and `\"Storage directory\"` literals** — Bound both to local variables (`display`, `name`) used across all return paths.\n\n**Net result: -38 lines (42 added, 80 removed), cleaner API, no redundant filesystem calls, no duplicate tests.**",
"last_stage": "simplify_opus",
"last_response": "All 24 tests pass. Here's a summary of what was fixed:\n\n## Summary of Changes\n\n### Issues Found & Fixed\n\n1. **Made `check_storage_dir` self-contained** (`doctor.rs`):\n - **Problem**: The function to"
},
"notes": "Stage completed: simplify_opus",
"usage": {
"model": "claude-opus-4-6",
"input_tokens": 48295,
"output_tokens": 13230,
"cache_read_tokens": 827171,
"cache_write_tokens": 52524,
"cost": 1.716675
},
"files_touched": [
"/home/daytona/workspace/lib/crates/fabro-cli/src/commands/doctor.rs",
"/home/daytona/workspace/lib/crates/fabro-cli/tests/it/cmd/doctor.rs"
]
}
},
"next_node_id": "verify",
"next_node_id": "fixup",
"loop_failure_signatures": {
"verify|deterministic|script failed with exit code: <n> ## stdout error[e0433]: failed to resolve: use of undeclared type `arc` --> lib/crates/fabro-server/tests/it/api.rs:<n>:<n> | <n> | key: arc::new(decoding_key),| ^^^ use of undeclared type `arc` | help: con": 1
},
"node_visits": {
"toolchain": 1,
"start": 1,
"implement": 1,
"verify": 1,
"simplify_opus": 1,
"preflight_lint": 1,
"simplify_gpt": 1,
"implement": 1,
"preflight_compile": 1,
"simplify_opus": 1,
"toolchain": 1,
"start": 1
"preflight_compile": 1
}
}

View file

@ -0,0 +1,357 @@
diff --git a/lib/crates/fabro-cli/src/commands/doctor.rs b/lib/crates/fabro-cli/src/commands/doctor.rs
index 70dc0da1..a627bb94 100644
--- a/lib/crates/fabro-cli/src/commands/doctor.rs
+++ b/lib/crates/fabro-cli/src/commands/doctor.rs
@@ -25,7 +25,7 @@ use semver::Version;
use crate::args::GlobalArgs;
use crate::shared::print_json_pretty;
-use crate::user_config::load_user_settings;
+use crate::user_config::load_user_settings_with_globals;
// ---------------------------------------------------------------------------
// System dependency types and parsers (server mode only)
@@ -242,61 +242,61 @@ pub(crate) fn check_config(
}
}
-pub(crate) fn check_storage_dir(path: &Path) -> CheckResult {
- let display = path.display();
- let summary = display.to_string();
- let name = "Storage directory".to_string();
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct StorageDirStatus {
+ path: PathBuf,
+ exists: bool,
+ readable: bool,
+ writable: bool,
+}
- let readable = std::fs::read_dir(path).is_ok();
- if !readable {
- // read_dir fails for both non-existent and permission-denied
- let exists = path.is_dir();
- let details = vec![CheckDetail::new(format!(
- "Exists: {}",
- if exists { "yes" } else { "no" }
- ))];
- return if exists {
- CheckResult {
- name,
- status: CheckStatus::Error,
- summary,
- details,
- remediation: Some(format!("Fix permissions on {display}")),
- }
- } else {
- CheckResult {
- name,
- status: CheckStatus::Error,
- summary,
- details,
- remediation: Some(format!("Create the directory: mkdir -p {display}")),
- }
- };
+fn probe_storage_dir(path: &Path) -> StorageDirStatus {
+ let exists = path.is_dir();
+ let readable = exists && std::fs::read_dir(path).is_ok();
+ let writable = exists && tempfile::tempfile_in(path).is_ok();
+
+ StorageDirStatus {
+ path: path.to_path_buf(),
+ exists,
+ readable,
+ writable,
}
+}
- let writable = tempfile::tempfile_in(path).is_ok();
+fn check_storage_dir(status: &StorageDirStatus) -> CheckResult {
+ let display = status.path.display();
let details = vec![
- CheckDetail::new("Exists: yes".to_string()),
- CheckDetail::new("Readable: yes".to_string()),
- CheckDetail::new(format!("Writable: {}", if writable { "yes" } else { "no" })),
+ CheckDetail::new(format!(
+ "Exists: {}",
+ if status.exists { "yes" } else { "no" }
+ )),
+ CheckDetail::new(format!(
+ "Readable: {}",
+ if status.readable { "yes" } else { "no" }
+ )),
+ CheckDetail::new(format!(
+ "Writable: {}",
+ if status.writable { "yes" } else { "no" }
+ )),
];
-
- if !writable {
- return CheckResult {
- name,
- status: CheckStatus::Error,
- summary,
- details,
- remediation: Some(format!("Fix permissions on {display}")),
- };
- }
+ let is_healthy = status.exists && status.readable && status.writable;
CheckResult {
- name,
- status: CheckStatus::Pass,
- summary,
+ name: "Storage directory".to_string(),
+ status: if is_healthy {
+ CheckStatus::Pass
+ } else {
+ CheckStatus::Error
+ },
+ summary: display.to_string(),
details,
- remediation: None,
+ remediation: if is_healthy {
+ None
+ } else if !status.exists {
+ Some(format!("Create the directory: mkdir -p {display}"))
+ } else {
+ Some(format!("Fix permissions on {display}"))
+ },
}
}
@@ -1031,17 +1031,14 @@ pub(crate) async fn run_doctor(
};
// Gather state
- let cli_settings = load_user_settings().unwrap_or_default();
+ let cli_settings = load_user_settings_with_globals(globals).unwrap_or_default();
let user_config_path = default_user_config_path();
let user_config_exists = user_config_path.as_ref().is_some_and(|p| p.exists());
let legacy_config_path = legacy_user_config_path();
let legacy_config_exists = legacy_config_path.as_ref().is_some_and(|p| p.exists());
- let storage_dir = globals
- .storage_dir
- .clone()
- .unwrap_or_else(|| cli_settings.storage_dir());
+ let storage_dir = probe_storage_dir(&cli_settings.storage_dir());
let llm_statuses: Vec<(Provider, bool)> = Provider::ALL
.iter()
@@ -1341,23 +1338,85 @@ mod tests {
// -- check_storage_dir --
#[test]
- fn check_storage_dir_pass() {
+ fn probe_storage_dir_existing_dir_is_readable_and_writable() {
let dir = tempfile::tempdir().unwrap();
- let result = check_storage_dir(dir.path());
+ let status = probe_storage_dir(dir.path());
+
+ assert_eq!(
+ status,
+ StorageDirStatus {
+ path: dir.path().to_path_buf(),
+ exists: true,
+ readable: true,
+ writable: true,
+ }
+ );
+ }
+
+ #[test]
+ fn probe_storage_dir_missing_dir_is_not_usable() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().join("missing");
+ let status = probe_storage_dir(&path);
+
+ assert_eq!(
+ status,
+ StorageDirStatus {
+ path,
+ exists: false,
+ readable: false,
+ writable: false,
+ }
+ );
+ }
+
+ #[test]
+ fn check_storage_dir_pass() {
+ let result = check_storage_dir(&StorageDirStatus {
+ path: PathBuf::from("/home/user/.fabro"),
+ exists: true,
+ readable: true,
+ writable: true,
+ });
+
assert_eq!(result.status, CheckStatus::Pass);
- assert!(result.summary.contains(dir.path().to_str().unwrap()));
+ assert_eq!(result.summary, "/home/user/.fabro");
assert!(result.remediation.is_none());
+ assert_eq!(result.details.len(), 3);
}
#[test]
fn check_storage_dir_not_exists() {
- let path = PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz");
- let result = check_storage_dir(&path);
+ let result = check_storage_dir(&StorageDirStatus {
+ path: PathBuf::from("/tmp/nonexistent-fabro-doctor-test-xyz"),
+ exists: false,
+ readable: false,
+ writable: false,
+ });
+
assert_eq!(result.status, CheckStatus::Error);
assert!(result.summary.contains("nonexistent-fabro-doctor-test-xyz"));
assert!(result.remediation.as_deref().unwrap().contains("mkdir -p"));
}
+ #[test]
+ fn check_storage_dir_not_writable() {
+ let result = check_storage_dir(&StorageDirStatus {
+ path: PathBuf::from("/home/user/.fabro"),
+ exists: true,
+ readable: true,
+ writable: false,
+ });
+
+ assert_eq!(result.status, CheckStatus::Error);
+ assert_eq!(result.summary, "/home/user/.fabro");
+ assert_eq!(
+ result.remediation.as_deref(),
+ Some("Fix permissions on /home/user/.fabro")
+ );
+ assert!(result.details.iter().any(|detail| detail.text == "Writable: no"));
+ }
+
// -- check_llm_providers --
#[test]
diff --git a/lib/crates/fabro-cli/tests/it/cmd/doctor.rs b/lib/crates/fabro-cli/tests/it/cmd/doctor.rs
index 34db450e..6f0eed8e 100644
--- a/lib/crates/fabro-cli/tests/it/cmd/doctor.rs
+++ b/lib/crates/fabro-cli/tests/it/cmd/doctor.rs
@@ -64,6 +64,119 @@ fn dry_run_flag() {
");
}
+#[test]
+fn storage_dir_shown_in_output() {
+ let context = test_context!();
+ let mut cmd = context.doctor();
+ cmd.args(["--dry-run", "--json"]);
+ cmd.env("ANTHROPIC_API_KEY", "sk-test-dummy");
+ fabro_snapshot!(context.filters(), cmd, @r#"
+ success: true
+ exit_code: 0
+ ----- stdout -----
+ {
+ "title": "Fabro Doctor",
+ "sections": [
+ {
+ "title": "Required",
+ "checks": [
+ {
+ "name": "Configuration",
+ "status": "warning",
+ "summary": "no user config file found",
+ "details": [
+ {
+ "text": "Create ~/.fabro/user.toml to configure Fabro",
+ "warn": false
+ }
+ ],
+ "remediation": "Create ~/.fabro/user.toml"
+ },
+ {
+ "name": "Storage directory",
+ "status": "pass",
+ "summary": "[STORAGE_DIR]",
+ "details": [
+ {
+ "text": "Exists: yes",
+ "warn": false
+ },
+ {
+ "text": "Readable: yes",
+ "warn": false
+ },
+ {
+ "text": "Writable: yes",
+ "warn": false
+ }
+ ],
+ "remediation": null
+ },
+ {
+ "name": "LLM providers",
+ "status": "pass",
+ "summary": "1 configured",
+ "details": [
+ {
+ "text": "anthropic (ANTHROPIC_API_KEY): set",
+ "warn": false
+ }
+ ],
+ "remediation": null
+ },
+ {
+ "name": "GitHub App",
+ "status": "warning",
+ "summary": "not configured",
+ "details": [
+ {
+ "text": "git.app_id: not set",
+ "warn": false
+ },
+ {
+ "text": "GITHUB_APP_PRIVATE_KEY: not set",
+ "warn": false
+ }
+ ],
+ "remediation": "Configure GitHub App in server.toml and set env vars to enable GitHub integration"
+ }
+ ]
+ },
+ {
+ "title": "Optional",
+ "checks": [
+ {
+ "name": "Cloud sandbox",
+ "status": "warning",
+ "summary": "no sandbox configured",
+ "details": [
+ {
+ "text": "Daytona (DAYTONA_API_KEY): not configured",
+ "warn": false
+ }
+ ],
+ "remediation": "Set DAYTONA_API_KEY to enable cloud sandbox execution"
+ },
+ {
+ "name": "Brave Search",
+ "status": "warning",
+ "summary": "not configured",
+ "details": [
+ {
+ "text": "BRAVE_SEARCH_API_KEY is not set",
+ "warn": false
+ }
+ ],
+ "remediation": "Set BRAVE_SEARCH_API_KEY to enable web search"
+ }
+ ]
+ }
+ ]
+ }
+ ----- stderr -----
+ "#);
+}
+
#[test]
fn storage_dir_missing_shows_error() {
let context = test_context!();

View file

@ -0,0 +1,5 @@
{
"command": "cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1",
"language": "shell",
"timeout_ms": null
}

View file

@ -0,0 +1,5 @@
{
"duration_ms": 140637,
"exit_code": 101,
"timed_out": false
}

6
nodes/verify/status.json Normal file
View file

@ -0,0 +1,6 @@
{
"status": "fail",
"notes": null,
"failure_reason": "Script failed with exit code: 101\n\n## stdout\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:389:22\n |\n389 | key: Arc::new(decoding_key),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nerror[E0433]: failed to resolve: use of undeclared type `Arc`\n --> lib/crates/fabro-server/tests/it/api.rs:390:29\n |\n390 | validation: Arc::new(fabro_server::jwt_auth::jwt_validation()),\n | ^^^ use of undeclared type `Arc`\n |\nhelp: consider importing this struct\n |\n 14 + use std::sync::Arc;\n |\n\nFor more information about this error, try `rustc --explain E0433`.\nerror: could not compile `fabro-server` (test \"it\") due to 2 previous errors\nerror: command `/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/cargo test --no-run --message-format json-render-diagnostics --workspace --quiet` exited with code 101\n",
"timestamp": "2026-04-01T13:38:59.780123Z"
}