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";
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.
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) }),
22describe("engine invariants", () => {
23 it("holds for random event sequences", () => {
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" },
30 for (const g of Object.values(s.groups)) g.dirty = true; // members joined, not yet named
31 for (const e of events) {
33 if (e.type === "name") {
34 // Simulate a naming reply landing: hash recorded, dirty cleared.
35 const g = s.groups[e.groupId];
37 g.membersHash = membersHash(membersOf(s, g.id));
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);
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));
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list