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>
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
/**
|
|
* #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);
|
|
});
|
|
});
|