Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925const std = @import("std");const auth_runtime = @import("../auth/auth_runtime.zig");const debug_trace = @import("../shared/debug_trace.zig");const io_mod = @import("../shared/io.zig");const agent_steps = @import("../config/agent_steps.zig");const config_runtime = @import("../config/config_runtime.zig");const host = @import("../hosts/host.zig");const mcp_contract = @import("../mcp/mcp_contract.zig");const session_store = @import("../session/session_store.zig");const types = @import("../shared/types.zig");const model_provider = @import("../config/model_provider.zig");
const Allocator = std.mem.Allocator;const default_session_diagnostics_limit: usize = 64;
pub const CheckStatus = enum { ok, warn, fail,};
pub const Check = struct { name: []const u8, status: CheckStatus, detail: []const u8, owned_detail: ?[]u8 = null,
pub fn deinit(self: *Check, alloc: Allocator) void { if (self.owned_detail) |detail| alloc.free(detail); self.* = undefined; }};
pub const Snapshot = struct { workspace_root: []u8, model: []const u8, provider: model_provider.ProviderId = .gateway, owned_model: ?[]u8 = null, auth: auth_runtime.StatusSnapshot = .{}, permission_mode: types.PermissionMode, agent_step_limit: usize, checks: []Check,
pub fn deinit(self: *Snapshot, alloc: Allocator) void { alloc.free(self.workspace_root); if (self.owned_model) |model| alloc.free(model); self.auth.deinit(alloc); for (self.checks) |*entry| entry.deinit(alloc); alloc.free(self.checks); self.* = undefined; }};
const ResolvedModel = struct { value: []const u8, owned: ?[]u8 = null,};
pub fn collect( alloc: Allocator, secret_store: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize, mcp_config_diagnostic: mcp_contract.ProfileConfigDiagnostic,) !Snapshot { const workspace_root = try io_mod.realpathAlloc(alloc, "."); debug_trace.configureFromEnv(alloc, workspace_root);
var checks: std.ArrayList(Check) = .empty; errdefer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); }
var snapshot = Snapshot{ .workspace_root = workspace_root, .model = default_model, .permission_mode = config_runtime.default_permission_mode, .agent_step_limit = default_agent_step_limit, .checks = &.{}, }; errdefer { alloc.free(snapshot.workspace_root); if (snapshot.owned_model) |model| alloc.free(model); snapshot.auth.deinit(alloc); }
const workspace_detail = try std.fmt.allocPrint(alloc, "using workspace {s}", .{snapshot.workspace_root}); try appendCheckOwned(&checks, alloc, "workspace", .ok, workspace_detail);
const paths = try config_runtime.discoverPaths(alloc, snapshot.workspace_root); defer { var mutable_paths = paths; mutable_paths.deinit(alloc); }
var detailed = config_runtime.loadMergedSettingsDetailed(alloc, snapshot.workspace_root) catch |err| { // Settings are unreadable, so no remembered choice is available to honour. snapshot.auth = try auth_runtime.loadStatusSnapshot(alloc, secret_store, null); try appendConfigLoadFailureCheck(&checks, alloc, "config", "failed to load config", err); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendConfigLoadFailureCheck(&checks, alloc, "startup", "failed to resolve startup settings", err); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc);
snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot; }; defer detailed.deinit(alloc); snapshot.provider = detailed.settings.provider orelse .gateway;
snapshot.auth = try auth_runtime.loadStatusSnapshotForProvider( alloc, secret_store, snapshot.provider, detailed.settings.credential_source, );
try appendConfigCheck(&checks, alloc, paths, detailed.diagnostics); try appendConfigDiagnosticChecks(&checks, alloc, detailed.diagnostics); try appendMcpConfigCheck(&checks, alloc, mcp_config_diagnostic); try appendAuthCheck(&checks, alloc, snapshot.auth); try appendResolvedStartupCheck(&snapshot, &checks, alloc, .{ .model = if (detailed.settings.models.get(snapshot.provider)) |model| @constCast(model) else null, .permission_mode = detailed.settings.permission_mode, .max_agent_steps = detailed.settings.max_agent_steps, }, default_model, default_agent_step_limit); try appendStateChecks(&checks, alloc, snapshot.workspace_root); try appendGitCheck(&checks, alloc, snapshot.workspace_root); try appendGhCheck(&checks, alloc);
snapshot.checks = try checks.toOwnedSlice(alloc); return snapshot;}
fn appendConfigDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, diagnostics: []const config_runtime.ConfigDiagnostic,) !void { for (diagnostics) |diagnostic| { var detail_writer: std.Io.Writer.Allocating = .init(alloc); defer detail_writer.deinit(); try detail_writer.writer.print( "{s} config diagnostic: {s}", .{ @tagName(diagnostic.layer), @tagName(diagnostic.cause) }, ); try config_runtime.writeDiagnosticMetadata(&detail_writer.writer, diagnostic); const detail = try detail_writer.toOwnedSlice(); try appendCheckOwned(checks, alloc, "config", .warn, detail); }}
fn appendConfigCheck( checks: *std.ArrayList(Check), alloc: Allocator, paths: config_runtime.Paths, diagnostics: []const config_runtime.ConfigDiagnostic,) !void { const user_exists = if (paths.user_settings) |path| try fileExists(path) and !configLayerRejected(diagnostics, .user) else false; const repo_exists = try fileExists(paths.workspace_settings) and !configLayerRejected(diagnostics, .project);
if (!user_exists and !repo_exists) { try appendCheck(checks, alloc, "config", .warn, "no config files found; using defaults and env overrides"); return; }
const detail = try formatConfigPresence(alloc, user_exists, repo_exists); try appendCheckOwned(checks, alloc, "config", .ok, detail);}
fn configLayerRejected( diagnostics: []const config_runtime.ConfigDiagnostic, layer: config_runtime.ConfigLayer,) bool { for (diagnostics) |diagnostic| { if (diagnostic.layer != layer) continue; switch (diagnostic.cause) { .malformed_settings, .settings_too_large, .durable_path_unsafe, .private_state_permissions_unsupported, .invalid_model_id, .retired_skill_match_fuzzy, .invalid_context_limits, => return true, .invalid_additional_directories, .ignored_project_user_only_setting, .legacy_workspace_preferences, .manual_backup_available, => {}, } } return false;}
fn appendAuthCheck(checks: *std.ArrayList(Check), alloc: Allocator, auth: auth_runtime.StatusSnapshot) !void { if (auth.missingHelp(.cli)) |help| { try appendCheck(checks, alloc, "auth", .fail, help); return; }
const detail = try auth.formatDoctorDetail(alloc); try appendCheckOwned(checks, alloc, "auth", if (auth.expired) .warn else .ok, detail);}
fn appendResolvedStartupCheck( snapshot: *Snapshot, checks: *std.ArrayList(Check), alloc: Allocator, settings: config_runtime.StartupStatusSettings, default_model: []const u8, default_agent_step_limit: usize,) !void { const next_model = try resolveModel(alloc, default_model, settings.model); if (snapshot.owned_model) |model| alloc.free(model); snapshot.model = next_model.value; snapshot.owned_model = next_model.owned; snapshot.permission_mode = try resolvePermissionMode(settings.permission_mode); snapshot.agent_step_limit = try resolveAgentStepLimit(default_agent_step_limit, settings.max_agent_steps);
const detail = try std.fmt.allocPrint( alloc, "resolved model={s}, permission_mode={s}, agent_step_limit={d}", .{ snapshot.model, permissionModeLabel(snapshot.permission_mode), snapshot.agent_step_limit }, ); try appendCheckOwned(checks, alloc, "startup", .ok, detail);}
fn appendConfigLoadFailureCheck(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, prefix: []const u8, err: anyerror) !void { const detail = try formatConfigLoadFailure(alloc, prefix, err); try appendCheckOwned(checks, alloc, name, .fail, detail);}
fn formatConfigLoadFailure(alloc: Allocator, prefix: []const u8, err: anyerror) ![]u8 { return std.fmt.allocPrint(alloc, "{s}: {s}", .{ prefix, @errorName(err) });}
fn appendStateChecks(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| { const status: CheckStatus = if (err == error.HomeNotSet) .warn else .fail; const detail = try std.fmt.allocPrint(alloc, "failed to inspect workspace state: {s}", .{@errorName(err)}); try appendCheckOwned(checks, alloc, "state", status, detail); return; }; defer store.deinit(alloc);
if (store.canonical_root.sessions == null) { try appendCheck(checks, alloc, "state", .warn, "durable state is not initialized"); try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; }
const state_detail = try std.fmt.allocPrint(alloc, "state dir ready at {s} (per-session managed state created on demand)", .{store.sessions_dir}); try appendCheckOwned(checks, alloc, "state", .ok, state_detail);
var inspection = try store.inspectForDoctorBounded(alloc, default_session_diagnostics_limit); defer inspection.deinit(alloc); try appendSessionDiagnosticChecks( checks, alloc, &store, inspection.diagnostics.items, ); if (inspection.truncated) { try appendSessionDiagnosticsTruncatedCheck(checks, alloc, inspection.inspected_count); }
var summaries = store.list(alloc) catch { try appendSessionsCountUnavailableCheck(checks, alloc); return; }; defer { for (summaries.items) |*summary| summary.deinit(alloc); summaries.deinit(alloc); } try appendSessionsCountCheck( checks, alloc, summaries.items.len, if (summaries.items.len > 0) summaries.items[0].id else "", );}
fn appendSessionDiagnosticsTruncatedCheck( checks: *std.ArrayList(Check), alloc: Allocator, inspected_count: usize,) !void { const detail = try std.fmt.allocPrint( alloc, "session diagnostics truncated after {d} session director{s} to keep doctor bounded", .{ inspected_count, if (inspected_count == 1) "y" else "ies" }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail);}
fn appendSessionsCountUnavailableCheck( checks: *std.ArrayList(Check), alloc: Allocator,) !void { try appendCheck( checks, alloc, "sessions", .warn, "exact saved-session count unavailable without a full session scan", );}
fn appendSessionsCountCheck( checks: *std.ArrayList(Check), alloc: Allocator, count: usize, latest_id: []const u8,) !void { if (count == 0) { try appendCheck(checks, alloc, "sessions", .warn, "no saved sessions yet"); return; }
const detail = try std.fmt.allocPrint(alloc, "{d} saved session(s); latest={s}", .{ count, latest_id }); try appendCheckOwned(checks, alloc, "sessions", .ok, detail);}
fn appendSessionDiagnosticChecks( checks: *std.ArrayList(Check), alloc: Allocator, read_store: *const session_store.Store, diagnostics: []const session_store.DoctorDiagnostic,) !void { for (diagnostics) |diagnostic| { if (diagnostic.kind == .cleanup_candidate) { const cleanup = read_store.cleanupForDoctor( alloc, diagnostic.session_id, ) catch |err| { const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate report_only=true error={s}", .{ diagnostic.session_id, @errorName(err) }, ); try appendCheckOwned(checks, alloc, "session", .warn, detail); continue; }; const detail = try std.fmt.allocPrint( alloc, "session {s}: cleanup_candidate cleanup_removed={d} report_only={d} ignored={d}", .{ diagnostic.session_id, cleanup.removed, cleanup.report_only, cleanup.ignored, }, ); try appendCheckOwned( checks, alloc, "session", if (cleanup.removed > 0) .ok else .warn, detail, ); continue; }
const status: CheckStatus = switch (diagnostic.kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .oversized_legacy_snapshot, .projection_missing, .projection_stale, .canonical_log_large, .canonical_log_compaction_overdue, .canonical_log_compaction_failed, .cleanup_candidate, => .warn, .invalid_authority_transition, .missing_authority, .invalid_authority, .invalid_commit_intent, .projection_invalid, .commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, .canonical_state_invalid, .unsafe_path, => .fail, };
var recovery_buffer: [512]u8 = undefined; const recovery = try recoveryActionForSessionDiagnostic( diagnostic.kind, diagnostic.session_id, &recovery_buffer, ); const detail = if (diagnostic.growth_bytes) |growth_bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d} growth_bytes={d} growth_frames={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), diagnostic.bytes.?, growth_bytes, diagnostic.growth_frames.?, recovery, }, ) else if (diagnostic.bytes) |bytes| try std.fmt.allocPrint( alloc, "session {s}: {s} bytes={d}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), bytes, recovery }, ) else try std.fmt.allocPrint( alloc, "session {s}: {s}{s}; recovery={s}", .{ diagnostic.session_id, @tagName(diagnostic.kind), if (diagnostic.kind == .authority_transition_pending) " report_only=true" else "", recovery, }, ); try appendCheckOwned(checks, alloc, "session", status, detail); }}
fn recoveryActionForSessionDiagnostic( kind: session_store.DoctorIssueKind, session_id: []const u8, buffer: []u8,) ![]const u8 { return switch (kind) { .authority_less_creation_orphan, .authority_transition_pending, .commit_intent_pending, .cleanup_candidate, => "rerun fx doctor after active writers exit; cleanup is guarded",
.canonical_log_large, .canonical_log_compaction_overdue, => "resume or update the session to trigger compaction",
.canonical_log_compaction_failed, => "inspect the failed compaction artifact; keep it until no writer is active",
.oversized_legacy_snapshot, => "rerun migration with --allow-large only after verifying the legacy snapshot",
.projection_missing, .projection_stale, => "open the session detail or resume it to rebuild projections",
.commit_watermark_missing, .commit_watermark_invalid, .commit_watermark_mismatched, => std.fmt.bufPrint( buffer, "run fx session recover {s}; it creates a separate resumable copy and leaves the source unchanged", .{session_id}, ),
.projection_invalid, .canonical_state_invalid, .invalid_commit_intent, => std.fmt.bufPrint( buffer, "back up ~/.fx/sessions, then inspect this session with fx session {s} --json", .{session_id}, ),
.missing_authority, .invalid_authority, .invalid_authority_transition, .unsafe_path, => "back up ~/.fx/sessions and avoid opening this session until the path is repaired", };}
fn appendGitCheck(checks: *std.ArrayList(Check), alloc: Allocator, workspace_root: []const u8) !void { if (try hasGitMetadata(alloc, workspace_root)) { try appendCheck(checks, alloc, "git", .ok, "git metadata detected for this workspace"); return; } try appendCheck(checks, alloc, "git", .warn, "not a git repository; pr/issue workflows will be limited");}
fn appendGhCheck(checks: *std.ArrayList(Check), alloc: Allocator) !void { if (try commandInPath(alloc, "gh")) { try appendCheck(checks, alloc, "gh", .ok, "GitHub CLI found in PATH"); return; } try appendCheck(checks, alloc, "gh", .warn, "GitHub CLI not found in PATH; publish workflows unavailable");}
fn resolveModel(alloc: Allocator, default_model: []const u8, configured: ?[]const u8) !ResolvedModel { if (io_mod.getenv("FX_MODEL")) |model| { const trimmed = std.mem.trim(u8, model, " \t\r\n"); if (trimmed.len > 0) return .{ .value = trimmed }; }
if (configured) |model| { const owned = try alloc.dupe(u8, model); return .{ .value = owned, .owned = owned }; }
return .{ .value = default_model };}
fn resolvePermissionMode(configured: ?types.PermissionMode) !types.PermissionMode { const fallback = configured orelse config_runtime.default_permission_mode; const raw = io_mod.getenv("FX_PERMISSION_MODE") orelse return fallback; return config_runtime.parsePermissionMode(raw) orelse fallback;}
fn resolveAgentStepLimit(fallback: usize, configured: ?usize) !usize { return agent_steps.resolveMaxAgentStepsWithOverride( configured, fallback, io_mod.getenv("FX_MAX_AGENT_STEPS"), );}
fn appendCheck(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, status: CheckStatus, detail: []const u8) !void { try checks.append(alloc, .{ .name = name, .status = status, .detail = detail, });}
fn appendCheckOwned(checks: *std.ArrayList(Check), alloc: Allocator, name: []const u8, status: CheckStatus, detail: []u8) !void { errdefer alloc.free(detail); try checks.append(alloc, .{ .name = name, .status = status, .detail = detail, .owned_detail = detail, });}
fn appendMcpConfigCheck( checks: *std.ArrayList(Check), alloc: Allocator, diagnostic: mcp_contract.ProfileConfigDiagnostic,) !void { switch (diagnostic) { .clear => return, .warning => |warning| { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.print( "~/.fx/mcp.json warning: {s}", .{@tagName(warning.cause)}, ); if (warning.key()) |key| try out.writer.print(" key={s}", .{key}); try out.writer.print( " additional_matches={d}", .{warning.additional_matches}, ); try appendCheckOwned( checks, alloc, "mcp_config", .warn, try out.toOwnedSlice(), ); return; }, .failed => |err| { const detail = try std.fmt.allocPrint( alloc, "failed to load ~/.fx/mcp.json: {s}", .{@errorName(err)}, ); try appendCheckOwned(checks, alloc, "mcp_config", .fail, detail); return; }, }}
fn formatConfigPresence(alloc: Allocator, user_exists: bool, repo_exists: bool) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit();
try out.writer.writeAll("loaded config from "); var first = true; if (user_exists) { if (!first) try out.writer.writeAll(", "); first = false; try out.writer.writeAll("~/.fx/settings.json"); } if (repo_exists) { if (!first) try out.writer.writeAll(", "); try out.writer.writeAll(".fx.json"); } return try out.toOwnedSlice();}
fn fileExists(path: []const u8) !bool { if (comptime @import("builtin").os.tag != .windows) { return accessPath(path); } std.Io.Dir.accessAbsolute(io_mod.getIo(), path, .{}) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; return true;}
fn hasGitMetadata(alloc: Allocator, workspace_root: []const u8) !bool { var path_buf: [std.fs.max_path_bytes]u8 = undefined; const git_path = std.fmt.bufPrint(&path_buf, "{s}" ++ std.fs.path.sep_str ++ ".git", .{workspace_root}) catch { const owned = try std.fs.path.join(alloc, &.{ workspace_root, ".git" }); defer alloc.free(owned); return try fileExists(owned); }; return try fileExists(git_path);}
fn commandInPath(alloc: Allocator, command_name: []const u8) !bool { const path_env = io_mod.getenv("PATH") orelse return false;
return commandInPathValue(alloc, command_name, path_env);}
fn commandInPathValue(alloc: Allocator, command_name: []const u8, path_env: []const u8) !bool { var it = std.mem.splitScalar(u8, path_env, std.fs.path.delimiter); while (it.next()) |entry| { if (entry.len == 0) continue;
var candidate_buf: [std.fs.max_path_bytes]u8 = undefined; const candidate = std.fmt.bufPrint(&candidate_buf, "{s}" ++ std.fs.path.sep_str ++ "{s}", .{ entry, command_name }) catch { const owned = try std.fs.path.join(alloc, &.{ entry, command_name }); defer alloc.free(owned); if (pathExists(owned)) return true; continue; };
if (pathExists(candidate)) { return true; } }
return false;}
fn pathExists(path: []const u8) bool { if (comptime @import("builtin").os.tag != .windows) { return accessPath(path) catch false; }
if (std.fs.path.isAbsolute(path)) { std.Io.Dir.accessAbsolute(io_mod.getIo(), path, .{}) catch return false; return true; }
std.Io.Dir.cwd().access(io_mod.getIo(), path, .{}) catch return false; return true;}
fn accessPath(path: []const u8) !bool { var path_buf: [std.fs.max_path_bytes]u8 = undefined; const path_z = std.fmt.bufPrintZ(&path_buf, "{s}", .{path}) catch return false; const rc = std.c.access(path_z.ptr, std.c.F_OK); if (rc == 0) return true; return switch (std.posix.errno(rc)) { .NOENT, .NOTDIR => false, .ACCES => false, else => |err| std.posix.unexpectedErrno(err), };}
fn permissionModeLabel(mode: types.PermissionMode) []const u8 { return switch (mode) { .ask => "ask", .auto => "auto", .yolo => "full access", };}
test "format config presence names existing layers" { const detail = try formatConfigPresence(std.testing.allocator, true, false); defer std.testing.allocator.free(detail);
try std.testing.expectEqualStrings("loaded config from ~/.fx/settings.json", detail);}
test "MCP config diagnostic maps only failures to one doctor check" { const alloc = std.testing.allocator; var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); }
try appendMcpConfigCheck(&checks, alloc, .clear); try std.testing.expectEqual(@as(usize, 0), checks.items.len);
try appendMcpConfigCheck( &checks, alloc, .{ .failed = error.McpConfigInvalidJson }, ); try std.testing.expectEqual(@as(usize, 1), checks.items.len); try std.testing.expectEqualStrings("mcp_config", checks.items[0].name); try std.testing.expectEqual(CheckStatus.fail, checks.items[0].status); try std.testing.expectEqualStrings( "failed to load ~/.fx/mcp.json: McpConfigInvalidJson", checks.items[0].detail, );}
test "config check handles user and workspace config files together" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeDoctorFixtureFile(tmp.dir, "home/.fx/settings.json", "{\"permission_mode\":\"ask\"}"); try writeDoctorFixtureFile(tmp.dir, "workspace/.fx.json", "{\"permission_mode\":\"auto\"}");
const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
var paths = try config_runtime.discoverPathsFromHome(std.testing.allocator, home_root, workspace_root); defer paths.deinit(std.testing.allocator);
var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(std.testing.allocator); checks.deinit(std.testing.allocator); }
try appendConfigCheck(&checks, std.testing.allocator, paths, &.{});
try std.testing.expectEqual(@as(usize, 1), checks.items.len); try std.testing.expectEqual(CheckStatus.ok, checks.items[0].status); try std.testing.expect(std.mem.find(u8, checks.items[0].detail, "~/.fx/settings.json") != null); try std.testing.expect(std.mem.find(u8, checks.items[0].detail, ".fx.json") != null);}
test "config check does not claim rejected user settings loaded" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try writeDoctorFixtureFile(tmp.dir, "home/.fx/settings.json", "{\"permission_mode\":\"ask\"}"); try writeDoctorFixtureFile(tmp.dir, "workspace/.fx.json", "{\"permission_mode\":\"auto\"}");
const home_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "home"); defer std.testing.allocator.free(home_root); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
var paths = try config_runtime.discoverPathsFromHome(std.testing.allocator, home_root, workspace_root); defer paths.deinit(std.testing.allocator);
var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(std.testing.allocator); checks.deinit(std.testing.allocator); }
const diagnostics = [_]config_runtime.ConfigDiagnostic{ .{ .layer = .user, .cause = .private_state_permissions_unsupported, }, }; try appendConfigCheck(&checks, std.testing.allocator, paths, &diagnostics);
try std.testing.expectEqual(@as(usize, 1), checks.items.len); try std.testing.expectEqual(CheckStatus.ok, checks.items[0].status); try std.testing.expect(std.mem.find(u8, checks.items[0].detail, "~/.fx/settings.json") == null); try std.testing.expect(std.mem.find(u8, checks.items[0].detail, ".fx.json") != null);}
test "session count check preserves empty and latest details" { const alloc = std.testing.allocator; var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); }
try appendSessionsCountCheck(&checks, alloc, 0, ""); try appendSessionsCountCheck(&checks, alloc, 2, "session-2");
try std.testing.expectEqual(@as(usize, 2), checks.items.len); try std.testing.expectEqualStrings("sessions", checks.items[0].name); try std.testing.expectEqual(CheckStatus.warn, checks.items[0].status); try std.testing.expectEqualStrings("no saved sessions yet", checks.items[0].detail); try std.testing.expectEqualStrings("sessions", checks.items[1].name); try std.testing.expectEqual(CheckStatus.ok, checks.items[1].status); try std.testing.expectEqualStrings("2 saved session(s); latest=session-2", checks.items[1].detail);}
test "bounded doctor warnings use existing check stream" { const alloc = std.testing.allocator; var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); }
try appendSessionDiagnosticsTruncatedCheck(&checks, alloc, 64); try appendSessionsCountUnavailableCheck(&checks, alloc);
try std.testing.expectEqual(@as(usize, 2), checks.items.len); try std.testing.expectEqualStrings("session", checks.items[0].name); try std.testing.expectEqual(CheckStatus.warn, checks.items[0].status); try std.testing.expect(std.mem.find( u8, checks.items[0].detail, "truncated after 64 session directories", ) != null); try std.testing.expectEqualStrings("sessions", checks.items[1].name); try std.testing.expectEqual(CheckStatus.warn, checks.items[1].status); try std.testing.expect(std.mem.find( u8, checks.items[1].detail, "unavailable without a full session scan", ) != null);}
test "session doctor renders precise watermark and compaction diagnostics" { const alloc = std.testing.allocator; var checks: std.ArrayList(Check) = .empty; defer { for (checks.items) |*entry| entry.deinit(alloc); checks.deinit(alloc); } var unused_store: session_store.Store = undefined; const diagnostics = [_]session_store.DoctorDiagnostic{ .{ .session_id = @constCast("missing-watermark"), .kind = .commit_watermark_missing, }, .{ .session_id = @constCast("failed-compaction"), .kind = .canonical_log_compaction_failed, .bytes = 1024, .growth_bytes = 512, .growth_frames = 9, }, };
try appendSessionDiagnosticChecks( &checks, alloc, &unused_store, &diagnostics, );
try std.testing.expectEqual(@as(usize, 2), checks.items.len); try std.testing.expectEqual(CheckStatus.fail, checks.items[0].status); try std.testing.expect(std.mem.find( u8, checks.items[0].detail, "commit_watermark_missing", ) != null); try std.testing.expect(std.mem.find( u8, checks.items[0].detail, "fx session recover missing-watermark", ) != null); try std.testing.expectEqual(CheckStatus.warn, checks.items[1].status); try std.testing.expect(std.mem.find( u8, checks.items[1].detail, "growth_bytes=512 growth_frames=9", ) != null);}
fn writeDoctorFixtureFile(dir: std.Io.Dir, sub_path: []const u8, text: []const u8) !void { var file = try dir.createFile(std.testing.io, sub_path, .{}); defer file.close(std.testing.io); try file.writeStreamingAll(std.testing.io, text);}
test "has git metadata detects .git file or directory" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "workspace/.git"); const workspace_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "workspace"); defer std.testing.allocator.free(workspace_root);
try std.testing.expect(try hasGitMetadata(std.testing.allocator, workspace_root));}
test "command in path checks explicit path entries" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup();
try tmp.dir.createDirPath(io_mod.getIo(), "bin"); var file = try tmp.dir.createFile(std.testing.io, "bin/gh", .{}); file.close(io_mod.getIo());
const bin_root = try io_mod.dirRealpathAlloc(std.testing.allocator, tmp.dir, "bin"); defer std.testing.allocator.free(bin_root);
const path_env = try std.fmt.allocPrint(std.testing.allocator, "{s}", .{bin_root}); defer std.testing.allocator.free(path_env);
try std.testing.expect(try commandInPathValue(std.testing.allocator, "gh", path_env)); try std.testing.expect(!(try commandInPathValue(std.testing.allocator, "missing-command", path_env)));}