v0.1.4-pre.018
This commit is contained in:
@@ -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 :
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
70
crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
Normal file
70
crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
Normal 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 } }));
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
72
crates/ksp-app-config-desk/tests/desktop_contract.rs
Normal file
72
crates/ksp-app-config-desk/tests/desktop_contract.rs
Normal 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\""));
|
||||
}
|
||||
80
crates/ksp-app-config-desk/tests/desktop_security.rs
Normal file
80
crates/ksp-app-config-desk/tests/desktop_security.rs
Normal 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);
|
||||
}
|
||||
@@ -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(_) => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user