summaryrefslogtreecommitdiffhomepage
path: root/src/features/layout/adapter/abi-port.test.ts
blob: 24f9c947184f01014ce95614301269aad7c9020e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { DEFAULT_LAYOUT, type LayoutTree } from "@dispatch/layout-contract";
import { describe, expect, it, vi } from "vitest";
import type { LayoutProviderState, LayoutStore } from "../store.svelte";
import { abiTree, createLayoutAbiPort } from "./abi-port";

/**
 * The ABI `layout` port tests (plan 04 §4.5) — the port is pure composition
 * over the layout store + the ops HTTP edge, so BOTH deps are fakes (the
 * outermost edges; nothing internal is mocked — asymmetric testing).
 */

function fakeStore(
  overrides: Partial<Pick<LayoutStore, "tree" | "providerState" | "activeWorkspaceId">> = {},
): LayoutStore {
  return {
    get tree(): LayoutTree {
      return DEFAULT_LAYOUT;
    },
    get providerState(): LayoutProviderState {
      return "present";
    },
    get activeWorkspaceId(): string {
      return "ws-a";
    },
    probe: vi.fn(async () => {}),
    onWorkspaceChanged: vi.fn(),
    onServerTree: vi.fn(),
    onTreeChange: vi.fn(),
    resetToDefault: vi.fn(async () => null),
    onExtensionsChanged: vi.fn(),
    onWorkspacePluginsChanged: vi.fn(),
    ...overrides,
  } as LayoutStore;
}

describe("abiTree — the getTree decision (pure)", () => {
  it("a PRESENT provider exposes the tree", () => {
    expect(abiTree("present", DEFAULT_LAYOUT)).toBe(DEFAULT_LAYOUT);
  });

  it("no-provider mode (absent) exposes NULL — the local fallback is not shared state", () => {
    expect(abiTree("absent", DEFAULT_LAYOUT)).toBeNull();
  });

  it("an unknown (not-yet-probed) provider exposes NULL too", () => {
    expect(abiTree("unknown", DEFAULT_LAYOUT)).toBeNull();
  });
});

describe("createLayoutAbiPort — applyOps → the ops route", () => {
  it("posts to the ACTIVE workspace and feeds the returned tree back through the store", async () => {
    const ops = [{ type: "ensurePanel", panels: "side", view: "demo" }] as const;
    const edited: LayoutTree = { ...DEFAULT_LAYOUT };
    const store = fakeStore();
    const seen: string[] = [];
    const port = createLayoutAbiPort({
      store,
      postOps: async (workspaceId, o) => {
        seen.push(workspaceId);
        return { ok: true, tree: edited, applied: o.length };
      },
    });

    const result = await port.applyOps(ops);
    expect(seen).toEqual(["ws-a"]); // the ACTIVE workspace at CALL time
    expect(result).toEqual({ ok: true, tree: edited, applied: 1 });
    // The successful apply echoes the tree into the store (instant local
    // re-render; the WS broadcast echo is idempotent with this).
    expect(store.onServerTree).toHaveBeenCalledWith("ws-a", edited);
  });

  it("a failed batch (typed no-provider error) does NOT touch the store", async () => {
    const store = fakeStore({ providerState: "absent" });
    const port = createLayoutAbiPort({
      store,
      postOps: async () => ({ ok: false, error: "no layout provider active" }),
    });

    const result = await port.applyOps([{ type: "removeNode", target: "side" }]);
    expect(result).toEqual({ ok: false, error: "no layout provider active" });
    expect(store.onServerTree).not.toHaveBeenCalled();
  });

  it("getTree returns the active tree while the provider is present, null when absent", () => {
    const port = createLayoutAbiPort({
      store: fakeStore({ providerState: "present" }),
      postOps: async () => ({ ok: false, error: "unused" }),
    });
    expect(port.getTree()).toBe(DEFAULT_LAYOUT);

    const absent = createLayoutAbiPort({
      store: fakeStore({ providerState: "absent" }),
      postOps: async () => ({ ok: false, error: "no layout provider active" }),
    });
    expect(absent.getTree()).toBeNull();
  });
});