fix(view-windows): réconcilier l'état des fenêtres détachées au boot (#50)

Branche le consommateur frontend sur la commande backend
`list_open_view_windows` : le port window expose la liste des fenêtres
de panneaux détachées réellement ouvertes côté OS, l'adaptateur Tauri
(et son double mock) l'implémente, et `ProjectsView` réconcilie le
placement des vues au démarrage à partir de ce snapshot au lieu de
supposer un état. Couvre `viewPlacement` et la réconciliation par des
tests dédiés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 18:14:45 +02:00
parent afbf315e97
commit b4a01e0ae9
7 changed files with 300 additions and 1 deletions

View File

@ -0,0 +1,89 @@
/**
* #50 — startup reconciliation of `placements` with the detached view windows
* already open at the OS level. Unit-tests the pure `isDetachablePanel` guard
* and `reconcileDetachedWindows`, covering every Architect invariant:
* only-visible, known-detachable-only (projects/unknown excluded), and the
* no-clobber rule (never overwrite a non-closed placement).
*/
import { describe, it, expect } from "vitest";
import type { ViewWindowSnapshot } from "@/ports";
import {
isDetachablePanel,
reconcileDetachedWindows,
type ViewPlacements,
} from "./viewPlacement";
const snap = (
panel: string,
visible = true,
): ViewWindowSnapshot => ({ panel, label: panel, visible });
describe("isDetachablePanel (#50)", () => {
it("accepts every known panel except projects", () => {
expect(isDetachablePanel("git")).toBe(true);
expect(isDetachablePanel("tickets")).toBe(true);
expect(isDetachablePanel("memory")).toBe(true);
});
it("excludes projects (main-window-only chrome) and unknown panels", () => {
expect(isDetachablePanel("projects")).toBe(false);
expect(isDetachablePanel("bogus")).toBe(false);
expect(isDetachablePanel("")).toBe(false);
});
});
describe("reconcileDetachedWindows (#50)", () => {
it("marks a visible detachable snapshot as detached from an empty state", () => {
const next = reconcileDetachedWindows({}, [snap("git")]);
expect(next).toEqual({ git: "detached" });
});
it("reconciles several panels at once", () => {
const next = reconcileDetachedWindows({}, [snap("git"), snap("tickets")]);
expect(next).toEqual({ git: "detached", tickets: "detached" });
});
it("ignores non-visible snapshots", () => {
const prev: ViewPlacements = {};
const next = reconcileDetachedWindows(prev, [snap("git", false)]);
expect(next).toBe(prev); // referentially unchanged
});
it("excludes the projects panel", () => {
const prev: ViewPlacements = {};
const next = reconcileDetachedWindows(prev, [snap("projects")]);
expect(next).toBe(prev);
});
it("ignores unknown panel ids", () => {
const prev: ViewPlacements = {};
const next = reconcileDetachedWindows(prev, [snap("bogus")]);
expect(next).toBe(prev);
});
it("never clobbers a non-closed placement (floating/dock/detached preserved)", () => {
const prev: ViewPlacements = {
git: "floating",
tickets: { dock: "left" },
memory: "detached",
};
const next = reconcileDetachedWindows(prev, [
snap("git"),
snap("tickets"),
snap("memory"),
]);
expect(next).toBe(prev); // nothing changed → same reference
});
it("reconciles only the closed panels, leaving others intact", () => {
const prev: ViewPlacements = { git: "floating" };
const next = reconcileDetachedWindows(prev, [snap("git"), snap("agents")]);
expect(next).toEqual({ git: "floating", agents: "detached" });
});
it("returns the same reference when there is nothing to reconcile", () => {
const prev: ViewPlacements = {};
expect(reconcileDetachedWindows(prev, [])).toBe(prev);
});
});