From 58e4d8ea79862e2fd1535f83c16a303098aac53e Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:29:45 +0200 Subject: [PATCH 1/3] test: add DIAGNOSIS_COMMANDS_MOVED architecture rule Test-First: scripts/ci/architecture-drift.test.mjs::rejects diagnosis commands in main.rs Co-authored-by: Cursor --- scripts/ci/architecture-drift.mjs | 2 ++ scripts/ci/architecture-drift.test.mjs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/scripts/ci/architecture-drift.mjs b/scripts/ci/architecture-drift.mjs index 58612ed7..eb470a21 100644 --- a/scripts/ci/architecture-drift.mjs +++ b/scripts/ci/architecture-drift.mjs @@ -15,6 +15,7 @@ const rules = { RAW_EVENT_EMIT: "new event families need an owner and TypeScript wrapper", HQ_DIRECT_DB: "HQ must use the API rather than the SQLite database", HQ_RAW_AUTH_FETCH: "only the Node proxy may attach the API token", + DIAGNOSIS_COMMANDS_MOVED: "diagnosis commands belong in diagnosis_cmds.rs", }; function files(dir = root) { @@ -136,6 +137,7 @@ function scan() { if (path === "src-tauri/src/main.rs") { const backend = block(source, /impl\s+ControlBackend\s+for\s+ApiBackend/); add("API_STORE_BLOCK_ON", path, callHits(backend, /tauri::async_runtime::block_on\s*\(/g, /self\.store/)); + add("DIAGNOSIS_COMMANDS_MOVED", path, regexHits(source, /fn\s+(?:get_log_path|get_panic_notice|get_reason_catalog|export_diagnosis|reveal_log_path|get_stuck_after_minutes|set_stuck_after_minutes)\s*\(/g)); } if (path === "src-tauri/src/api.rs" || path.startsWith("src-tauri/src/api/")) add("ERROR_TEXT_CLASSIFIER", path, regexHits(source, /err(?:or)?\.(?:contains|starts_with)\s*\(/g)); diff --git a/scripts/ci/architecture-drift.test.mjs b/scripts/ci/architecture-drift.test.mjs index 8ea35b8b..b82e6a4d 100644 --- a/scripts/ci/architecture-drift.test.mjs +++ b/scripts/ci/architecture-drift.test.mjs @@ -111,3 +111,10 @@ test("a unicode escape char literal does not confuse test mod stripping", () => assert.equal(result.status, 1); assert.equal((result.stderr.match(/RAW_PROCESS_SPAWN/g) ?? []).length, 1); }); + +test("rejects diagnosis commands in main.rs", () => { + const root = fixture({ "src-tauri/src/main.rs": "fn get_reason_catalog() { }\n" }); + const result = run(root, "check"); + assert.equal(result.status, 1); + assert.match(result.stderr, /DIAGNOSIS_COMMANDS_MOVED.*src-tauri\/src\/main\.rs/); +}); From 2a8111fd818a48c063adf1e62e2960a6b7547da2 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:31:47 +0200 Subject: [PATCH 2/3] feat: move diagnosis commands to diagnosis_cmds module This extracts get_log_path, get_panic_notice, get_reason_catalog, export_diagnosis, reveal_log_path, get_stuck_after_minutes, and set_stuck_after_minutes from main.rs into a new diagnosis_cmds.rs module to shrink main.rs. No-Test: covered by the previous commit's architectural rule Co-authored-by: Cursor --- src-tauri/src/diagnosis_cmds.rs | 99 +++++++++++++++++++++++++++++++++ src-tauri/src/main.rs | 90 +----------------------------- 2 files changed, 101 insertions(+), 88 deletions(-) create mode 100644 src-tauri/src/diagnosis_cmds.rs diff --git a/src-tauri/src/diagnosis_cmds.rs b/src-tauri/src/diagnosis_cmds.rs new file mode 100644 index 00000000..9f805cdb --- /dev/null +++ b/src-tauri/src/diagnosis_cmds.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; +use tauri::{AppHandle, State}; + +use crate::app_data_dir; +use crate::diagnosis; +use crate::logging; +use crate::providers::KeyVault; +use crate::status::StatusEngine; +use crate::store::Store; +use crate::stuck; +use crate::PanicNotice; + +#[tauri::command] +pub fn get_log_path(app: AppHandle) -> Result { + let dir = app_data_dir(&app)?; + Ok(logging::log_file(&dir).display().to_string()) +} + +#[tauri::command] +pub fn get_panic_notice(notice: State<'_, PanicNotice>) -> PanicNotice { + notice.inner().clone() +} + +#[tauri::command] +pub fn get_reason_catalog() -> Vec { + diagnosis::reason_catalog() +} + +#[tauri::command] +pub async fn export_diagnosis( + app: AppHandle, + store: State<'_, Store>, + engine: State<'_, Arc>, + vault: State<'_, Arc>, + notice: State<'_, PanicNotice>, +) -> Result { + let dir = app_data_dir(&app)?; + let panic = notice.inner().clone(); + let (pack, secrets) = diagnosis::collect( + &dir, + store.inner(), + engine.inner().as_ref(), + vault.inner().as_ref(), + &panic, + ) + .await?; + Ok(diagnosis::export_json(&pack, &secrets)) +} + +/// Open the log file in the OS file manager so the user does not have to +/// know `%APPDATA%`. Explorer's `/select,` highlights the file; elsewhere +/// the parent directory opens. +#[tauri::command] +pub fn reveal_log_path(app: AppHandle) -> Result<(), String> { + let dir = app_data_dir(&app)?; + let path = logging::log_file(&dir); + #[cfg(windows)] + { + crate::proc::command("explorer") + .arg(format!("/select,{}", path.display())) + .spawn() + .map_err(|e| format!("failed to open Explorer: {e}"))?; + Ok(()) + } + #[cfg(target_os = "macos")] + { + crate::proc::command("open") + .arg("-R") + .arg(&path) + .spawn() + .map_err(|e| format!("failed to reveal the log file: {e}"))?; + Ok(()) + } + #[cfg(all(unix, not(target_os = "macos")))] + { + let parent = path.parent().unwrap_or(path.as_path()); + crate::proc::command("xdg-open") + .arg(parent) + .spawn() + .map_err(|e| format!("failed to open the log directory: {e}"))?; + Ok(()) + } +} + +/// After how many quiet minutes a running worker is called stuck, or `null` +/// when the built-in default applies. +#[tauri::command] +pub async fn get_stuck_after_minutes(store: State<'_, Store>) -> Result, String> { + Ok(stuck::threshold_minutes(&store).await) +} + +/// Set (or with `null`, clear) that threshold. +#[tauri::command] +pub async fn set_stuck_after_minutes( + store: State<'_, Store>, + minutes: Option, +) -> Result<(), String> { + stuck::set_threshold_minutes(&store, minutes).await +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c9a7515b..9ba3cb9f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -69,6 +69,7 @@ mod development_plan; mod development_plan_access; mod development_policy; mod diagnosis; +mod diagnosis_cmds; mod diff; mod digest; mod enhance; @@ -117,6 +118,7 @@ mod web_interface; mod workers; mod worktree; +use diagnosis_cmds::*; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -1953,43 +1955,6 @@ fn parse_stats_range(range: Option<&str>) -> Result { // -- P2-H diagnosis (log path, pack, Warum) -------------------------------- -#[tauri::command] -fn get_log_path(app: AppHandle) -> Result { - let dir = app_data_dir(&app)?; - Ok(logging::log_file(&dir).display().to_string()) -} - -#[tauri::command] -fn get_panic_notice(notice: State<'_, PanicNotice>) -> PanicNotice { - notice.inner().clone() -} - -#[tauri::command] -fn get_reason_catalog() -> Vec { - diagnosis::reason_catalog() -} - -#[tauri::command] -async fn export_diagnosis( - app: AppHandle, - store: State<'_, Store>, - engine: State<'_, Arc>, - vault: State<'_, Arc>, - notice: State<'_, PanicNotice>, -) -> Result { - let dir = app_data_dir(&app)?; - let panic = notice.inner().clone(); - let (pack, secrets) = diagnosis::collect( - &dir, - store.inner(), - engine.inner().as_ref(), - vault.inner().as_ref(), - &panic, - ) - .await?; - Ok(diagnosis::export_json(&pack, &secrets)) -} - /// F5 baseline: CPU/RAM/disk plus the OmniRoute token totals. No ceilings. #[tauri::command] async fn get_resource_snapshot( @@ -2018,59 +1983,8 @@ async fn delete_session_buffers() -> Result { .map_err(|e| format!("deleting the session buffers did not finish: {e}"))? } -/// Open the log file in the OS file manager so the user does not have to -/// know `%APPDATA%`. Explorer's `/select,` highlights the file; elsewhere -/// the parent directory opens. -#[tauri::command] -fn reveal_log_path(app: AppHandle) -> Result<(), String> { - let dir = app_data_dir(&app)?; - let path = logging::log_file(&dir); - #[cfg(windows)] - { - crate::proc::command("explorer") - .arg(format!("/select,{}", path.display())) - .spawn() - .map_err(|e| format!("failed to open Explorer: {e}"))?; - Ok(()) - } - #[cfg(target_os = "macos")] - { - crate::proc::command("open") - .arg("-R") - .arg(&path) - .spawn() - .map_err(|e| format!("failed to reveal the log file: {e}"))?; - Ok(()) - } - #[cfg(all(unix, not(target_os = "macos")))] - { - let parent = path.parent().unwrap_or(path.as_path()); - crate::proc::command("xdg-open") - .arg(parent) - .spawn() - .map_err(|e| format!("failed to open the log directory: {e}"))?; - Ok(()) - } -} - // -- stuck diagnosis commands (Phase 18) ----------------------------------- -/// After how many quiet minutes a running worker is called stuck, or `null` -/// when the built-in default applies. -#[tauri::command] -async fn get_stuck_after_minutes(store: State<'_, Store>) -> Result, String> { - Ok(stuck::threshold_minutes(&store).await) -} - -/// Set (or with `null`, clear) that threshold. -#[tauri::command] -async fn set_stuck_after_minutes( - store: State<'_, Store>, - minutes: Option, -) -> Result<(), String> { - stuck::set_threshold_minutes(&store, minutes).await -} - // -- provider commands (Phase 7.2) ----------------------------------------- /// Which providers this machine can reach, and what each one is doing. From d14722607e95a571fe359fdcd7d8c1245e8f29a6 Mon Sep 17 00:00:00 2001 From: Cuarroc <47299143+Cuarroc@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:26:13 +0200 Subject: [PATCH 3/3] fix: address review findings for V16-ARCH-D5a - Expand architecture-drift test to check the exact new location and generate_handler! export for all 7 diagnosis commands. - Remove orphaned section headers in main.rs. Test-First: scripts/ci/architecture-drift.test.mjs::contract: all 7 diagnosis commands are properly moved to diagnosis_cmds.rs and exported in main.rs Co-authored-by: Cursor --- scripts/ci/architecture-drift.test.mjs | 35 +++++++++++++++++++++++++- src-tauri/src/main.rs | 4 --- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/scripts/ci/architecture-drift.test.mjs b/scripts/ci/architecture-drift.test.mjs index b82e6a4d..a867fe79 100644 --- a/scripts/ci/architecture-drift.test.mjs +++ b/scripts/ci/architecture-drift.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -118,3 +118,36 @@ test("rejects diagnosis commands in main.rs", () => { assert.equal(result.status, 1); assert.match(result.stderr, /DIAGNOSIS_COMMANDS_MOVED.*src-tauri\/src\/main\.rs/); }); + +test("contract: all 7 diagnosis commands are properly moved to diagnosis_cmds.rs and exported in main.rs", () => { + const repoRoot = process.env.TEST_REPO_ROOT || dirname(dirname(dirname(fileURLToPath(import.meta.url)))); + const mainRs = readFileSync(join(repoRoot, "src-tauri/src/main.rs"), "utf8"); + let diagnosisRs = ""; + try { + diagnosisRs = readFileSync(join(repoRoot, "src-tauri/src/diagnosis_cmds.rs"), "utf8"); + } catch (e) { + // missing in older tree, handled below + } + + const commands = [ + "get_log_path", + "get_panic_notice", + "get_reason_catalog", + "export_diagnosis", + "reveal_log_path", + "get_stuck_after_minutes", + "set_stuck_after_minutes" + ]; + + for (const cmd of commands) { + assert.doesNotMatch(mainRs, new RegExp(`fn\\s+${cmd}\\s*\\(`), `Command ${cmd} should not be defined in main.rs`); + assert.match(diagnosisRs, new RegExp(`#\\[tauri::command\\]\\s*(?:(?:#\\[[^\\]]*\\]|\\/\\/.*|\\/\\*.*?\\*\\/)\\s*)*(?:pub\\s+)?(?:async\\s+)?fn\\s+${cmd}\\s*\\(`), `Command ${cmd} should be exported in diagnosis_cmds.rs`); + } + + const handlerMatch = mainRs.match(/generate_handler!\[(.*?)\]/s); + assert.ok(handlerMatch, "generate_handler! not found in main.rs"); + const handlerList = handlerMatch[1]; + for (const cmd of commands) { + assert.match(handlerList, new RegExp(`\\b${cmd}\\b`), `Command ${cmd} should be listed in generate_handler!`); + } +}); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9ba3cb9f..9e65d8e8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1953,8 +1953,6 @@ fn parse_stats_range(range: Option<&str>) -> Result { } } -// -- P2-H diagnosis (log path, pack, Warum) -------------------------------- - /// F5 baseline: CPU/RAM/disk plus the OmniRoute token totals. No ceilings. #[tauri::command] async fn get_resource_snapshot( @@ -1983,8 +1981,6 @@ async fn delete_session_buffers() -> Result { .map_err(|e| format!("deleting the session buffers did not finish: {e}"))? } -// -- stuck diagnosis commands (Phase 18) ----------------------------------- - // -- provider commands (Phase 7.2) ----------------------------------------- /// Which providers this machine can reach, and what each one is doing.