on 2026.09.23.2009 help sign in
diagonal/test/property.test.ts
63 lines 2.9 kB history raw
1import fc from "fast-check";
2import { describe, expect, it } from "vitest";
3import { applyEvent, type EngineEvent } from "../src/background/engine";
4import { membersHash } from "../src/background/naming";
5import { membersOf } from "../src/background/state";
6import { ctx, stateWith, tab } from "./helpers";
8/**
9 * For any event sequence: no tab is in two groups, and every managed group with ≥ 2 members is
10 * either dirty or has a membersHash equal to its current membership.
11 */
12const TAB_IDS = [1, 2, 3, 4, 5, 6];
13const GROUP_IDS = [-1, 10, 11];
15const event = fc.oneof(
16 fc.record({ type: fc.constant("tabUpdated" as const), id: fc.constantFrom(...TAB_IDS), groupId: fc.constantFrom(...GROUP_IDS), title: fc.constantFrom("a", "b", "c"), path: fc.constantFrom("x", "y") }),
17 fc.record({ type: fc.constant("tabRemoved" as const), id: fc.constantFrom(...TAB_IDS) }),
18 fc.record({ type: fc.constant("meta" as const), id: fc.constantFrom(...TAB_IDS), description: fc.constantFrom("one", "two") }),
19 fc.record({ type: fc.constant("name" as const), groupId: fc.constantFrom(10, 11) }),
20);
22describe("engine invariants", () => {
23 it("holds for random event sequences", () => {
24 fc.assert(
25 fc.property(fc.array(event, { maxLength: 40 }), (events) => {
26 const s = stateWith(TAB_IDS.map((id) => tab(id, { groupId: id % 2 ? 10 : 11 })), [
27 { id: 10, origin: "opener" },
28 { id: 11, origin: "organize" },
29 ]);
30 for (const g of Object.values(s.groups)) g.dirty = true; // members joined, not yet named
31 for (const e of events) {
32 let ev: EngineEvent;
33 if (e.type === "name") {
34 // Simulate a naming reply landing: hash recorded, dirty cleared.
35 const g = s.groups[e.groupId];
36 if (g) {
37 g.membersHash = membersHash(membersOf(s, g.id));
38 g.dirty = false;
39 }
40 continue;
41 }
42 if (e.type === "tabUpdated") {
43 if (e.groupId !== -1 && !s.groups[e.groupId]) continue;
44 ev = { type: "tabUpdated", tab: tab(e.id, { groupId: e.groupId, title: e.title, url: `https://example.com/${e.path}/${e.id}` }) };
45 } else if (e.type === "tabRemoved") ev = { type: "tabRemoved", tabId: e.id };
46 else ev = { type: "meta", tabId: e.id, description: e.description };
47 applyEvent(s, ev, ctx());
49 const seen = new Set<number>();
50 for (const t of Object.values(s.tabs)) {
51 expect(seen.has(t.id)).toBe(false);
52 seen.add(t.id);
53 }
54 for (const g of Object.values(s.groups)) {
55 const members = membersOf(s, g.id);
56 if (g.managed && members.length >= 2 && !g.dirty) expect(g.membersHash).toBe(membersHash(members));
57 }
58 }
59 }),
60 { numRuns: 300 },
61 );
62 });
63});