File size: 3,953 Bytes
d197cf3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
98
99
100
101
import { afterEach, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import {
  closeOpenClawAgentDatabasesForTest,
  openOpenClawAgentDatabase,
} from "../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { createTestBoardStore, readBoardHtml } from "./board-store.test-support.js";

const tempDirs = useAutoCleanupTempDirTracker(afterEach);
afterEach(() => {
  closeOpenClawAgentDatabasesForTest();
  closeOpenClawStateDatabaseForTest();
});

it("round-trips and removes a board with many tabs", async () => {
  const store = createTestBoardStore();
  const target = { sessionKey: "agent:main:many-tabs" };
  const tabIds = Array.from({ length: 130 }, (_, index) => `tab-${index}`);
  const created = await store.applyOps(
    target,
    tabIds.map((tabId) => ({ kind: "tab_create", tabId, title: tabId })),
  );
  expect(created.tabs.map((tab) => tab.tabId)).toEqual(tabIds);
  expect(await store.getSnapshot(target)).toEqual(created);
  expect(
    await store.applyOps(
      target,
      tabIds.map((tabId) => ({ kind: "tab_delete", tabId })),
    ),
  ).toEqual({
    ...target,
    revision: 2,
    tabs: [],
    widgets: [],
  });
  expect(await store.getSnapshot(target)).toEqual({
    ...target,
    revision: 0,
    tabs: [],
    widgets: [],
  });
});

it("rolls back late board writes and preserves surviving widget documents", async () => {
  const stateDir = tempDirs.make("openclaw-board-batch-rollback-");
  const env = { OPENCLAW_STATE_DIR: stateDir };
  const sessionKey = "agent:main:batch-rollback";
  const store = createTestBoardStore({ stateDir });

  const tabIds = Array.from({ length: 130 }, (_, index) => `tab-${index}`);
  await store.applyOps(
    { sessionKey },
    tabIds.map((tabId) => ({
      kind: "tab_create",
      tabId,
      title: tabId,
    })),
  );
  const widgetNames = tabIds.slice(0, 48);
  for (const name of widgetNames) {
    await store.putWidget({ sessionKey, name, content: { kind: "html", html: name } });
  }
  const before = await store.getSnapshot({ sessionKey });
  const database = openOpenClawAgentDatabase({ agentId: "main", env });
  const readRows = () => ({
    tabs: database.db.prepare("SELECT * FROM board_tabs ORDER BY session_key, tab_id").all(),
    widgets: database.db.prepare("SELECT * FROM board_widgets ORDER BY session_key, name").all(),
  });
  const beforeRows = readRows();
  database.db.exec(`CREATE TEMP TRIGGER reject_late_tab_write BEFORE UPDATE ON board_tabs
      WHEN NEW.tab_id = 'tab-129' BEGIN SELECT RAISE(ABORT, 'late tab write'); END;`);
  await expect(
    store.applyOps({ sessionKey }, [
      {
        kind: "tab_update",
        tabId: "tab-0",
        title: "changed",
      },
    ]),
  ).rejects.toThrow("late tab write");
  expect(readRows()).toEqual(beforeRows);
  database.db.exec("DROP TRIGGER reject_late_tab_write");
  expect(await store.getSnapshot({ sessionKey })).toEqual(before);

  const removedNames = widgetNames.slice(0, -1);
  const removeOps = removedNames.map((name) => ({ kind: "widget_remove" as const, name }));
  database.db.exec(`CREATE TEMP TRIGGER reject_late_widget_delete BEFORE DELETE ON board_widgets
      WHEN OLD.name = 'tab-46' BEGIN SELECT RAISE(ABORT, 'late widget delete'); END;`);
  await expect(store.applyOps({ sessionKey }, removeOps)).rejects.toThrow("late widget delete");
  expect(readRows()).toEqual(beforeRows);
  database.db.exec("DROP TRIGGER reject_late_widget_delete");
  const removed = await store.applyOps({ sessionKey }, removeOps);
  expect(removed.widgets.map((widget) => widget.name)).toEqual(["tab-47"]);
  expect(await store.getSnapshot({ sessionKey })).toEqual(removed);
  expect(await readBoardHtml(store, { sessionKey }, "tab-47")).toMatchObject({
    html: "tab-47",
  });
  expect(await readBoardHtml(store, { sessionKey }, "tab-46")).toBeUndefined();
});