PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / unit / Agent / state / workflows-persistence.test.js

workflows-persistence.test.js in Extendify 3.1.0, at tests/unit/Agent/state/workflows-persistence.test.js

115 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // Pins the selection-state persistence boundary that the Agent-store merge
2 // (main removed `workflowHistory`, this branch moved `block` out to
3 // `useQuickEditStore.agentBlock`) left implicit. Two guarantees, neither
4 // enforced by a `partialize` anymore:
5 //
6 // 1. The QuickEdit selection model is structurally non-persisted — its slots
7 // reset on every reload, even with a stale agent-workflows blob sitting in
8 // localStorage. (If the store ever gained a `persist` wrapper, this fails.)
9 // 2. A pre-move workflow blob carrying stale `block` / `workflowHistory`
10 // hydrates the live `workflow` slot cleanly, and those dead keys do NOT
11 // feed the block-availability path the app actually reads — availability
12 // keys off the live (null) `useQuickEditStore.agentBlock`, not the blob.
13 //
14 // `@agent/workflows/workflows` uses webpack `require.context`, which doesn't
15 // exist under Jest, so the registry + the two onboarding theme specs are mocked
16 // here. `zustand/middleware` is intentionally NOT mocked — the point is to
17 // exercise the real `persist` hydration + `merge`.
18
19 jest.mock('@agent/workflows/workflows', () => ({
20 workflows: [
21 { id: 'plain', available: () => true },
22 { id: 'needs-block', available: () => true, requires: ['block'] },
23 ],
24 tools: {},
25 }));
26
27 const themeSpec = {
28 __esModule: true,
29 default: { example: { agentResponse: { whenFinishedTool: {} } } },
30 };
31 jest.mock('@agent/workflows/theme/change-site-design', () => themeSpec);
32 jest.mock('@agent/workflows/theme/change-theme-variation', () => themeSpec);
33 jest.mock('@agent/lib/util', () => ({
34 isChangeSiteDesignWorkflowAvailable: () => false,
35 }));
36
37 const PERSIST_KEY = 'extendify-agent-workflows-test-site';
38
39 // A blob shaped like one written *before* the block→QuickEdit move: it carries
40 // the live workflow slot plus the now-dead `block` / `blockCode` /
41 // `workflowHistory` keys that no code reads anymore.
42 const seedPreMoveBlob = () => {
43 localStorage.setItem(
44 PERSIST_KEY,
45 JSON.stringify({
46 version: 0,
47 state: {
48 workflow: { id: 'needs-block', startingPage: 'https://x.test/' },
49 workflowData: null,
50 whenFinishedToolProps: null,
51 block: { id: 'stale-block', el: {} },
52 blockCode: '<p>stale</p>',
53 workflowHistory: [{ id: 'old-completed' }],
54 },
55 }),
56 );
57 };
58
59 beforeEach(() => {
60 jest.resetModules();
61 localStorage.clear();
62 window.extAgentData = { chatHistory: [{ role: 'user' }] }; // non-empty → not onboarding
63 window.extSharedData = { ...window.extSharedData, siteId: 'test-site' };
64 });
65
66 describe('QuickEdit selection store — non-persisted by construction', () => {
67 it('exposes no persist API and starts every selection slot null on reload', async () => {
68 const { useQuickEditStore } = await import('@quick-edit/state/store');
69 // A `persist()`-wrapped store hangs a `.persist` control API off the
70 // hook; a plain `create()` store has none. This is the structural guard
71 // that the selection model never gains a persist wrapper.
72 expect(useQuickEditStore.persist).toBeUndefined();
73 const s = useQuickEditStore.getState();
74 expect(s.selected).toBeNull();
75 expect(s.agentBlock).toBeNull();
76 expect(s.agentBlockCode).toBeNull();
77 expect(s.committedSelection).toBeNull();
78 });
79 });
80
81 describe('Agent workflow store — pre-move blob hydration', () => {
82 it('hydrates the live workflow slot and does not resurrect block/workflowHistory into availability', async () => {
83 seedPreMoveBlob();
84 const { useWorkflowStore } = await import('@agent/state/workflows');
85 const { useQuickEditStore } = await import('@quick-edit/state/store');
86
87 // The live slot hydrates from the blob.
88 expect(useWorkflowStore.getState().workflow).toMatchObject({
89 id: 'needs-block',
90 });
91
92 // The stale persisted `block` did not leak into the slot the app reads.
93 expect(useQuickEditStore.getState().agentBlock).toBeNull();
94
95 // Block-availability keys off the live (null) QuickEdit block, NOT the
96 // stale persisted one — so a block-gated workflow stays excluded.
97 expect(
98 useWorkflowStore
99 .getState()
100 .getAvailableWorkflows()
101 .map((w) => w.id),
102 ).toEqual(['plain']);
103
104 // Control: a *live* agentBlock flips availability, proving the source of
105 // truth is the non-persisted QuickEdit store, not the persisted blob.
106 useQuickEditStore.setState({ agentBlock: { id: 'live' } });
107 expect(
108 useWorkflowStore
109 .getState()
110 .getAvailableWorkflows()
111 .map((w) => w.id),
112 ).toEqual(['needs-block']);
113 });
114 });
115