v0.1.4-pre.018

This commit is contained in:
2026-08-16 20:15:58 +02:00
parent af5e828864
commit 035e25cb1f
15 changed files with 582 additions and 96 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 89
# version: 90
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package]
version = "0.1.4-pre.17"
version = "0.1.4-pre.18"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 20 -->
<!-- version: 21 -->
# `ksp-app-config-desk`
@@ -53,13 +53,16 @@ frontend/
│ ├── main.scss
│ └── splash.scss
├── ts/
│ ├── documents.ts
│ ├── environment.ts
│ ├── frontend_log.ts
│ ├── invoke.ts
│ ├── logging.ts
│ ├── logging_test.ts
│ ├── main.ts
│ ├── profiles.ts
│ ├── secret_reveal.ts
│ ├── shell_registry.ts
│ └── splash.ts
├── main.html
└── splash.html
@@ -73,6 +76,14 @@ frontend/ts/bindings/ksp_app_config_desk/...
Les DTO Rust applicatifs restent la source de vérité et les bindings générés ne sont pas versionnés.
## Robustesse desktop et extensibilité
`pre.018` rend le fallback de bootstrap Logging testable avant installation du subscriber : la résolution produit d'abord un plan `managed` ou `fallback`, puis seulement l'initialisation runtime est tentée. Une source Logging invalide peut ainsi être couverte par test sans installer un subscriber global dans le processus de tests. Le fallback reste transitoire, console-only, niveau `Info`, sans file sink.
Le frontend possède désormais `shell_registry.ts`. Le registre décrit les vues du shell et les adapters d'éditeurs spécialisés par `file_id`. Le panneau Documents reste générique ; lorsqu'un document possède un adapter enregistré, **Ouvrir l'éditeur spécialisé** déclenche la navigation par événement de registre. `cfg.std.logging -> Logging` est le premier adapter. Ajouter un futur éditeur ne demande donc pas de réécrire le moteur Documents ni la logique générale d'activation des panneaux.
Deux audits d'intégration applicatifs complètent les audits Config/Logging existants : contrat Tauri/Vite/package scripts, centralisation des commandes Tauri, absence de `tauri-plugin-log`, interdiction des dialogues navigateur natifs et absence de stockage persistant pour le reveal Secret. Le script frontend `npm run check` exécute `tsc --noEmit` puis le build Vite.
## Artefacts frontend
Les artefacts frontend construits ne sont pas versionnés. `tauri.conf.json` fixe :

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-app-config-desk/TODO.md -->
<!-- version: 17 -->
<!-- version: 18 -->
# TODO `ksp-app-config-desk`
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime, les fichiers isolés par identité de lancement et le panneau Test Logging backend/frontend sont maintenant présents.
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime, les fichiers isolés par identité de lancement, le panneau Test Logging backend/frontend, le registre d'éditeurs spécialisés et les audits desktop de robustesse sont maintenant présents.
Les prochaines tranches doivent encore :
La clôture `pre.019` doit encore :
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
- étudier puis intégrer le retour Rust -> console WebKit via `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, afin de pouvoir utiliser `attachConsole()` sans second subscriber, double émission ni boucle avec le bridge KSP.

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 20 -->
<!-- version: 21 -->
# Utilisation de `ksp-app-config-desk`
## État actuel
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile`, une identité de lancement observable pour isoler les fichiers persistants et un panneau de test du routing backend/frontend.
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile`, une identité de lancement observable pour isoler les fichiers persistants et un panneau de test du routing backend/frontend. Le shell possède aussi un registre d'éditeurs spécialisés par `file_id`, utilisé aujourd'hui pour `cfg.std.logging`.
La fenêtre `splash` est visible au démarrage. Après readiness du frontend et temporisation résolue par Config, l'application effectue le fade-out, affiche/focalise `main` puis détruit `splash`. La fenêtre principale expose immédiatement la navigation monofenêtre de référence.
@@ -155,6 +155,11 @@ La sélection charge le source brut via `ConfigManagement::read_source()`, y com
Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source.
### Ouvrir un éditeur spécialisé depuis Documents
Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton.
## Panneau Profils
La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné :
@@ -277,3 +282,22 @@ Scénario attendu avec deux profils persistés `local_dev` et `local_test` :
Le bouton d'application explicite est désactivé lorsque le brouillon est sale afin qu'un profil affiché mais non persisté ne soit jamais confondu avec un profil réellement chargeable par Config.
Si la résolution ou la préparation du profil explicite échoue, `ksp_logging_lib::reinitialize()` ne remplace pas les layers actifs et la génération ne doit pas avancer. Une sauvegarde de document qui échoue pendant l'application runtime conserve également l'ancien runtime et restaure la source précédente lorsqu'elle avait été modifiée.
## Validation desktop de robustesse
Depuis `crates/ksp-app-config-desk`, le contrôle frontend explicite est :
```bash
npm run check
```
Il exécute `tsc --noEmit` puis `vite build`. La validation intégrée de la tranche inclut aussi :
```bash
cargo test -p ksp-app-config-desk
cargo test -p ksp-config-lib
cargo test -p ksp-logging-lib
cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json
```
Un source Logging invalide au démarrage doit sélectionner le fallback transitoire sans persistence automatique. Après réparation du document, un nouveau lancement doit reprendre la configuration managée.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 15 -->
<!-- version: 16 -->
<!DOCTYPE html>
<html lang="fr">
@@ -90,6 +90,7 @@
<span id="documentDetailStatus" class="badge text-bg-secondary"></span>
</div>
<div class="d-flex gap-2">
<button id="openSpecializedEditor" class="btn btn-outline-primary btn-sm" type="button" hidden>Ouvrir l'éditeur spécialisé</button>
<button id="reloadDocumentSource" class="btn btn-outline-secondary btn-sm" type="button">Recharger</button>
<button id="saveDocumentSource" class="btn btn-primary btn-sm" type="button">Valider et sauvegarder</button>
</div>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/documents.ts
// version: 1
// version: 2
//! Documents panel backed exclusively by ksp-config-lib inventory, validation and management APIs.
@@ -13,6 +13,7 @@ import type { ConfigDocumentSaveResultDto } from "./bindings/ksp_app_config_desk
import type { ConfigDocumentSummaryDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto";
import { frontendDebug, frontendTrace, frontendWarn } from "./frontend_log";
import { invokeKsp } from "./invoke";
import { requestViewActivation, specializedEditorForFileId } from "./shell_registry";
interface DocumentsDataTable {
destroy(): unknown;
@@ -84,6 +85,12 @@ function renderDetail(detail: ConfigDocumentDetailDto): void {
if (saveButton) {
saveButton.disabled = detail.source === null;
}
const specializedEditor = specializedEditorForFileId(detail.summary.fileId);
const specializedButton = document.querySelector<HTMLButtonElement>("#openSpecializedEditor");
if (specializedButton) {
specializedButton.hidden = specializedEditor === null;
specializedButton.textContent = specializedEditor?.label ?? "Ouvrir l'éditeur spécialisé";
}
const detailPanel = document.querySelector<HTMLElement>("#documentDetailPanel");
if (detailPanel) {
detailPanel.hidden = false;
@@ -270,6 +277,15 @@ function bindDocumentActions(): void {
frontendTrace("main", "Document save button clicked");
void saveActiveDocument();
});
document.querySelector<HTMLButtonElement>("#openSpecializedEditor")?.addEventListener("click", () => {
const fileId = activeDocument?.summary.fileId ?? "";
const descriptor = specializedEditorForFileId(fileId);
if (!descriptor) {
return;
}
frontendDebug("main", "Specialized Config editor requested", { fileId, viewId: descriptor.viewId });
requestViewActivation(descriptor.viewId);
});
frontendTrace("main", "Documents panel handlers installed");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 12
// version: 13
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -12,60 +12,32 @@ import { initializeEnvironmentPanel } from "./environment";
import { initializeLoggingPanel } from "./logging";
import { initializeProfilesPanel } from "./profiles";
import { invokeKsp } from "./invoke";
import { isViewId, registeredViewIds, viewRegistry, type ViewId } from "./shell_registry";
import { clearTransientSecretReveal } from "./secret_reveal";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
const viewCopy: Record<ViewId, { title: string; description: string }> = {
overview: {
title: "Vue d'ensemble",
description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
},
documents: {
title: "Documents",
description: "Inventaire, diagnostics et réparation validée des documents Config enregistrés.",
},
profiles: {
title: "Profils",
description: "Sélection default/explicite, vues global/profil/effective sûre et provenance Config.",
},
environment: {
title: "Environnement / .env",
description: "Rapport sûr desired/effective/source/shadow et management atomique des variables .env KSP/KSPB.",
},
logging: {
title: "Logging",
description: "Édition typée, hot reload runtime, profils actifs et panneau de test du routing Logging KSP.",
},
};
function isViewId(value: string): value is ViewId {
return Object.prototype.hasOwnProperty.call(viewCopy, value);
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
function activateView(viewId: ViewId, source: "startup" | "user" | "registry"): void {
if (viewId !== "environment") {
clearTransientSecretReveal();
}
if (source === "user") {
frontendDebug("main", "Main navigation activated", { viewId });
if (source !== "startup") {
frontendDebug("main", "Main navigation activated", { viewId, source });
}
const copy = viewCopy[viewId];
const descriptor = viewRegistry[viewId];
const title = document.querySelector<HTMLElement>("#viewTitle");
const headerTitle = document.querySelector<HTMLElement>("#headerViewTitle");
const description = document.querySelector<HTMLElement>("#viewDescription");
if (title) {
title.textContent = copy.title;
title.textContent = descriptor.title;
}
if (headerTitle) {
headerTitle.textContent = copy.title;
headerTitle.textContent = descriptor.title;
}
document.title = `Config Desk — ${copy.title}`;
document.title = `Config Desk — ${descriptor.title}`;
if (description) {
description.textContent = copy.description;
description.textContent = descriptor.description;
}
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
@@ -73,25 +45,11 @@ function activateView(viewId: ViewId, source: "startup" | "user"): void {
button.setAttribute("aria-current", active ? "page" : "false");
});
frontendTrace("main", "Main navigation tab state changed", { viewId, source });
const overview = document.querySelector<HTMLElement>("#overviewPanel");
if (overview) {
overview.hidden = viewId !== "overview";
}
const documents = document.querySelector<HTMLElement>("#documentsPanel");
if (documents) {
documents.hidden = viewId !== "documents";
}
const profiles = document.querySelector<HTMLElement>("#profilesPanel");
if (profiles) {
profiles.hidden = viewId !== "profiles";
}
const environment = document.querySelector<HTMLElement>("#environmentPanel");
if (environment) {
environment.hidden = viewId !== "environment";
}
const logging = document.querySelector<HTMLElement>("#loggingPanel");
if (logging) {
logging.hidden = viewId !== "logging";
for (const registeredViewId of registeredViewIds()) {
const panel = document.querySelector<HTMLElement>(`#${viewRegistry[registeredViewId].panelId}`);
if (panel) {
panel.hidden = registeredViewId !== viewId;
}
}
frontendTrace("main", "Main view DOM updated", { viewId, source });
}
@@ -106,6 +64,13 @@ function bindNavigation(): void {
}
});
});
window.addEventListener("ksp:activate-view", event => {
const detail = event instanceof CustomEvent ? event.detail : null;
const requestedView = typeof detail === "object" && detail !== null && "viewId" in detail ? String(detail.viewId) : "";
if (isViewId(requestedView)) {
activateView(requestedView, "registry");
}
});
frontendTrace("main", "Main navigation handlers installed");
}

View File

@@ -0,0 +1,70 @@
// file: crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
// version: 1
//! Declarative shell and specialized-editor registry for Config Desk frontend extensibility.
export type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
export interface ViewDescriptor {
title: string;
description: string;
panelId: string;
}
export interface SpecializedEditorDescriptor {
fileId: string;
viewId: ViewId;
label: string;
}
export const viewRegistry: Record<ViewId, ViewDescriptor> = {
overview: {
title: "Vue d'ensemble",
description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
panelId: "overviewPanel",
},
documents: {
title: "Documents",
description: "Inventaire, diagnostics et réparation validée des documents Config enregistrés.",
panelId: "documentsPanel",
},
profiles: {
title: "Profils",
description: "Sélection default/explicite, vues global/profil/effective sûre et provenance Config.",
panelId: "profilesPanel",
},
environment: {
title: "Environnement / .env",
description: "Rapport sûr desired/effective/source/shadow et management atomique des variables .env KSP/KSPB.",
panelId: "environmentPanel",
},
logging: {
title: "Logging",
description: "Édition typée, hot reload runtime, profils actifs et panneau de test du routing Logging KSP.",
panelId: "loggingPanel",
},
};
const specializedEditorRegistry: SpecializedEditorDescriptor[] = [
{
fileId: "cfg.std.logging",
viewId: "logging",
label: "Ouvrir l'éditeur Logging",
},
];
export function registeredViewIds(): ViewId[] {
return Object.keys(viewRegistry) as ViewId[];
}
export function isViewId(value: string): value is ViewId {
return Object.prototype.hasOwnProperty.call(viewRegistry, value);
}
export function specializedEditorForFileId(fileId: string): SpecializedEditorDescriptor | null {
return specializedEditorRegistry.find(descriptor => descriptor.fileId === fileId) ?? null;
}
export function requestViewActivation(viewId: ViewId): void {
window.dispatchEvent(new CustomEvent("ksp:activate-view", { detail: { viewId } }));
}

View File

@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"tauri": "tauri",
"check": "tsc --noEmit && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 2
// version: 3
//! Config and Logging bootstrap for the desktop application.
@@ -11,6 +11,18 @@ pub(crate) struct LoggingStartup {
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
enum LoggingStartupPlan {
Managed {
active_profile_id: String,
settings: ksp_logging_lib::LoggingSettings,
},
Fallback {
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
},
}
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap {
@@ -30,37 +42,52 @@ pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> {
let plan = resolve_logging_startup(management);
return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
match guard {
std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
active_profile = active_profile_id.as_str(),
"initialized Config Desk logging from managed configuration"
);
std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::Some(active_profile_id),
selection_source: "default_profile".to_owned(),
fallback_active: false,
startup_diagnostic: std::option::Option::None,
})
},
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
}
},
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
},
};
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
};
let active_profile_id = resolved.profile_id().to_owned();
let settings = resolved.into_settings();
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
return match guard {
std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
active_profile = active_profile_id.as_str(),
"initialized Config Desk logging from managed configuration"
);
std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::Some(active_profile_id),
selection_source: "default_profile".to_owned(),
fallback_active: false,
startup_diagnostic: std::option::Option::None,
})
},
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
}
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
}
fn initialize_fallback_logging(
@@ -68,7 +95,15 @@ fn initialize_fallback_logging(
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
let settings = fallback_logging_settings();
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
}
fn initialize_planned_fallback_logging(
initial_error: ksp_core_lib::Error,
diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard {
std::result::Result::Ok(value) => value,

View File

@@ -0,0 +1,72 @@
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
// version: 1
//! Desktop build/shell contract audits for Config Desk.
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
fn read_json(path: &std::path::Path) -> serde_json::Value {
let source = read_text(path);
let parsed = serde_json::from_str::<serde_json::Value>(source.as_str());
assert!(parsed.is_ok(), "unable to parse {}", path.display());
return match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => serde_json::Value::Null,
};
}
#[test]
fn tauri_and_frontend_build_contracts_remain_explicit() {
let root = app_root();
let tauri = read_json(root.join("tauri.conf.json").as_path());
assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1430"));
assert_eq!(tauri.pointer("/build/beforeDevCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
assert_eq!(tauri.pointer("/build/beforeBuildCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
assert!(windows.is_some());
let windows = match windows {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(windows.len(), 2);
let labels = windows.iter().filter_map(|window| window.get("label").and_then(serde_json::Value::as_str)).collect::<std::vec::Vec<_>>();
assert_eq!(labels, ["splash", "main"]);
let main = windows.iter().find(|window| window.get("label").and_then(serde_json::Value::as_str) == std::option::Option::Some("main"));
assert!(main.is_some());
if let std::option::Option::Some(main) = main {
assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false));
}
let package = read_json(root.join("package.json").as_path());
let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str);
let check = package.pointer("/scripts/check").and_then(serde_json::Value::as_str);
assert!(build.is_some_and(|value| value.contains("tsc") && value.contains("vite build")));
assert!(check.is_some_and(|value| value.contains("tsc --noEmit") && value.contains("vite build")));
}
#[test]
fn shell_registry_owns_view_activation_and_specialized_editor_dispatch() {
let root = app_root();
let registry = read_text(root.join("frontend/ts/shell_registry.ts").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let documents = read_text(root.join("frontend/ts/documents.ts").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
assert!(registry.contains("export const viewRegistry"));
assert!(registry.contains("fileId: \"cfg.std.logging\""));
assert!(registry.contains("viewId: \"logging\""));
assert!(main.contains("registeredViewIds()"));
assert!(main.contains("ksp:activate-view"));
assert!(documents.contains("specializedEditorForFileId"));
assert!(documents.contains("requestViewActivation"));
assert!(html.contains("id=\"openSpecializedEditor\""));
}

View File

@@ -0,0 +1,80 @@
// file: crates/ksp-app-config-desk/tests/desktop_security.rs
// version: 1
//! Desktop security/ownership regression audits local to Config Desk.
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn collect_files(directory: &std::path::Path, extension: &str, files: &mut std::vec::Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(directory);
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
collect_files(path.as_path(), extension, files);
continue;
}
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some(extension) {
files.push(path);
}
}
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
#[test]
fn frontend_avoids_native_dialogs_and_persistent_secret_storage() {
let root = app_root();
let mut typescript = std::vec::Vec::new();
collect_files(root.join("frontend/ts").as_path(), "ts", &mut typescript);
assert!(!typescript.is_empty());
for path in typescript {
let source = read_text(path.as_path());
for forbidden in ["window.alert(", "window.confirm(", "window.prompt("] {
assert!(!source.contains(forbidden), "{} uses forbidden browser-native dialog {forbidden}", path.display());
}
}
let secret_reveal = read_text(root.join("frontend/ts/secret_reveal.ts").as_path());
assert!(!secret_reveal.contains("localStorage"));
assert!(!secret_reveal.contains("sessionStorage"));
assert!(secret_reveal.contains("clearTransientSecretReveal"));
}
#[test]
fn tauri_commands_and_logging_adapters_remain_on_owned_boundaries() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("ksp-logging-lib"));
assert!(manifest.contains("tauri-plugin-tracing"));
assert!(!manifest.contains("tauri-plugin-log"));
let source_root = root.join("src");
let mut rust_files = std::vec::Vec::new();
collect_files(source_root.as_path(), "rs", &mut rust_files);
let mut command_attribute_count = 0usize;
for path in rust_files {
let source = read_text(path.as_path());
let count = source.matches("#[tauri::command]").count();
if path.file_name().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("tauri.rs") {
command_attribute_count += count;
} else {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
}
assert!(command_attribute_count > 0);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/bootstrap.rs
// version: 1
// version: 2
#[test]
fn fallback_logging_is_console_only_and_bounded_to_info() {
@@ -15,3 +15,103 @@ fn fallback_logging_is_console_only_and_bounded_to_info() {
assert_eq!(console.format(), ksp_logging_lib::LogFormat::Human);
}
}
#[test]
fn invalid_logging_source_is_planned_as_transient_fallback_before_subscriber_installation() {
let fixture = BootstrapFixture::new("invalid-source");
assert!(fixture.is_ok());
let fixture = match fixture {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let malformed = std::fs::write(fixture.config_root.join(ksp_config_lib::DEFAULT_STD_LOGGING_FILENAME), "{ invalid json");
assert!(malformed.is_ok());
let management = fixture.management();
assert!(management.is_ok());
let management = match management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let plan = super::resolve_logging_startup(&management);
let fallback = matches!(&plan, super::LoggingStartupPlan::Fallback { .. });
assert!(fallback, "invalid Logging source must produce a fallback startup plan");
if let super::LoggingStartupPlan::Fallback { diagnostic, settings, .. } = plan {
assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
assert!(settings.files().is_empty());
assert!(!diagnostic.domain.is_empty());
assert!(!diagnostic.code.is_empty());
assert!(!diagnostic.message.is_empty());
}
}
struct BootstrapFixture {
root: std::path::PathBuf,
config_root: std::path::PathBuf,
schema_root: std::path::PathBuf,
}
impl BootstrapFixture {
fn new(label: &str) -> ksp_core_lib::Result<Self> {
let root = std::env::temp_dir().join(std::format!("ksp-config-desk-{label}-{}", std::process::id()));
let cleanup = std::fs::remove_dir_all(root.as_path());
match cleanup {
std::result::Result::Ok(()) | std::result::Result::Err(_) => {},
}
let config_root = root.join("config");
let schema_root = root.join("schemas");
let create_config = std::fs::create_dir_all(config_root.as_path());
if let std::result::Result::Err(error) = create_config {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot create bootstrap test config root").with_source(error),
);
}
let create_schema = std::fs::create_dir_all(schema_root.as_path());
if let std::result::Result::Err(error) = create_schema {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot create bootstrap test schema root").with_source(error),
);
}
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = match workspace.parent().and_then(std::path::Path::parent) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Cannot resolve workspace root for bootstrap test",
));
},
};
let source_schema = workspace.join(ksp_config_lib::DEFAULT_SCHEMA_PATH).join(ksp_config_lib::DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
let destination_schema = schema_root.join(ksp_config_lib::DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
let copied = std::fs::copy(source_schema.as_path(), destination_schema.as_path());
if let std::result::Result::Err(error) = copied {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot copy bootstrap test Logging schema").with_source(error),
);
}
return std::result::Result::Ok(Self { root, config_root, schema_root });
}
fn management(&self) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(self.config_root.as_path(), self.schema_root.as_path());
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
}
}
impl std::ops::Drop for BootstrapFixture {
fn drop(&mut self) {
let cleanup = std::fs::remove_dir_all(self.root.as_path());
match cleanup {
std::result::Result::Ok(()) | std::result::Result::Err(_) => {},
}
}
}

105
deltas/0.1.4/pre.018.md Normal file
View File

@@ -0,0 +1,105 @@
<!-- file: deltas/0.1.4/pre.018.md -->
<!-- version: 1 -->
# Delta `0.1.4-pre.018` — robustesse, extensibilité et tests desktop
## Base
`0.1.4-pre.017` est validée : `cargo fmt --all`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont propres ; 62 tests unitaires Config Desk, 88 tests Config, 37 tests unitaires Logging et tous les audits/tests d'intégration associés passent. Le scénario runtime montre également le routing `TRACE` backend puis frontend après hot reload.
## Objectif
Durcir Config Desk avant la tranche de clôture : tester explicitement le comportement de démarrage sur source Logging invalide, matérialiser le registre d'éditeurs spécialisés prévu par le plan, et ajouter des audits desktop automatisés sur Tauri, le frontend, les secrets et les frontières d'ownership.
## Changements
### Bootstrap Logging testable avant installation
Le bootstrap sépare désormais la résolution d'un `LoggingStartupPlan` de l'installation du subscriber :
- `Managed` contient le profil résolu et les `LoggingSettings` ;
- `Fallback` contient le diagnostic sûr, l'erreur initiale et les settings transitoires.
L'installation runtime intervient seulement après cette décision. Un test peut donc fournir un source Logging invalide et vérifier la sélection du fallback sans installer un subscriber global dans le processus de tests.
Le fallback reste inchangé fonctionnellement : console stderr, niveau `Info`, aucun fichier et spans désactivés.
### Registre shell / éditeurs spécialisés
Nouveau `frontend/ts/shell_registry.ts` :
- registre unique des cinq vues du shell ;
- activation générique des panels par `panelId` ;
- registre séparé `file_id -> éditeur spécialisé` ;
- premier adapter `cfg.std.logging -> logging` ;
- événement `ksp:activate-view` pour demander une navigation sans coupler Documents à l'implémentation de Logging.
Le panneau Documents affiche **Ouvrir l'éditeur Logging** uniquement lorsqu'un adapter spécialisé existe pour le `file_id` sélectionné. Les documents inconnus du registre spécialisé restent inspectables et réparables via le shell générique.
### Audits desktop
Deux nouveaux tests d'intégration Config Desk couvrent :
- contrat Tauri `splash/main`, main initialement caché et commandes Vite explicites ;
- scripts frontend `build` et `check` ;
- présence du registre shell et dispatch d'éditeur spécialisé ;
- absence de `window.alert`, `window.confirm`, `window.prompt` ;
- absence de `localStorage` / `sessionStorage` dans le reveal Secret ;
- centralisation de tous les `#[tauri::command]` dans `tauri.rs` ;
- présence de `ksp-logging-lib` + `tauri-plugin-tracing` et absence de `tauri-plugin-log`.
Les audits Config ownership et Logging ownership existants restent les autorités workspace pour les frontières Config/tracing.
### Frontend build
`package.json` ajoute :
```text
npm run check
```
qui exécute `tsc --noEmit && vite build`.
## Version technique
```text
workspace.package.version = 0.1.4-pre.18
```
## Validation attendue
Depuis la racine du workspace :
```bash
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-config-desk
cargo test -p ksp-config-lib
cargo test -p ksp-logging-lib
```
Depuis `crates/ksp-app-config-desk` :
```bash
npm run check
```
Puis depuis la racine :
```bash
cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json
cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json
```
Parcours fonctionnel minimal :
1. Documents -> sélectionner `cfg.std.logging` ;
2. vérifier **Ouvrir l'éditeur Logging** ;
3. cliquer et vérifier l'activation de la vue Logging ;
4. revenir à Documents et confirmer que diagnostics/source restent inchangés ;
5. vérifier le comportement Logging déjà validé (profil runtime, hot reload, Test Logging) après ce refactor de shell.
## Suite
`0.1.4-pre.019` est la tranche de clôture : validation workspace/build complète, matrice fonctionnelle finale, retour du target applicatif de `debug` vers `info`/`warn`, README/USAGE/TODO/changelog, nettoyage/archivage et préparation de `rel.001` ainsi que du prompt de session suivant.

View File

@@ -1023,6 +1023,12 @@ DTO UI
Aucun nouvel éditeur ne reçoit une API de filesystem directe ni ne transporte son propre validateur autoritatif.
### 15.4 Implémentation `pre.018`
Le shell applicatif matérialise désormais cette architecture dans `frontend/ts/shell_registry.ts` : les vues sont décrites dans un registre unique et les adapters spécialisés sont déclarés séparément par `file_id`. Le panneau Documents interroge ce registre pour afficher ou non **Ouvrir l'éditeur spécialisé** ; il ne contient aucune branche métier Logging. Le premier mapping est `cfg.std.logging -> logging`.
Le bootstrap Logging sépare également la **résolution du plan de démarrage** de l'**installation du subscriber**. Une source invalide peut ainsi être testée comme plan fallback sans initialiser le subscriber global du binaire de tests.
## 16. Stratégie de tests
### 16.1 `ksp-config-lib`
@@ -1253,7 +1259,7 @@ pre.016 Logging runtime — compléments [réalisé]
- observabilité metadata/génération du runtime
- consolidation des scénarios de rollback/runtime
pre.017 panneau Test Logging [en cours]
pre.017 panneau Test Logging [réalisé]
- champ message
- niveau trace/debug/info/warn/error/tous
- select target KSP contrôlé
@@ -1262,7 +1268,7 @@ pre.017 panneau Test Logging [en cours]
- preuve routing avant/après hot reload
- preuve bridge frontend
pre.018 robustesse/extensibilité/tests desktop
pre.018 robustesse/extensibilité/tests desktop [en cours]
- invalid source au démarrage
- audits secrets/Config ownership/tracing
- tests frontend/Tauri/build