@plures/praxis 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/FRAMEWORK.md +420 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1310 -0
  4. package/dist/adapters/cli.d.ts +43 -0
  5. package/dist/adapters/cli.d.ts.map +1 -0
  6. package/dist/adapters/cli.js +126 -0
  7. package/dist/adapters/cli.js.map +1 -0
  8. package/dist/cli/commands/auth.d.ts +26 -0
  9. package/dist/cli/commands/auth.d.ts.map +1 -0
  10. package/dist/cli/commands/auth.js +233 -0
  11. package/dist/cli/commands/auth.js.map +1 -0
  12. package/dist/cli/commands/cloud.d.ts +27 -0
  13. package/dist/cli/commands/cloud.d.ts.map +1 -0
  14. package/dist/cli/commands/cloud.js +232 -0
  15. package/dist/cli/commands/cloud.js.map +1 -0
  16. package/dist/cli/commands/generate.d.ts +25 -0
  17. package/dist/cli/commands/generate.d.ts.map +1 -0
  18. package/dist/cli/commands/generate.js +168 -0
  19. package/dist/cli/commands/generate.js.map +1 -0
  20. package/dist/cli/index.d.ts +8 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +179 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cloud/auth.d.ts +51 -0
  25. package/dist/cloud/auth.d.ts.map +1 -0
  26. package/dist/cloud/auth.js +194 -0
  27. package/dist/cloud/auth.js.map +1 -0
  28. package/dist/cloud/billing.d.ts +184 -0
  29. package/dist/cloud/billing.d.ts.map +1 -0
  30. package/dist/cloud/billing.js +179 -0
  31. package/dist/cloud/billing.js.map +1 -0
  32. package/dist/cloud/client.d.ts +39 -0
  33. package/dist/cloud/client.d.ts.map +1 -0
  34. package/dist/cloud/client.js +176 -0
  35. package/dist/cloud/client.js.map +1 -0
  36. package/dist/cloud/index.d.ts +44 -0
  37. package/dist/cloud/index.d.ts.map +1 -0
  38. package/dist/cloud/index.js +44 -0
  39. package/dist/cloud/index.js.map +1 -0
  40. package/dist/cloud/marketplace.d.ts +166 -0
  41. package/dist/cloud/marketplace.d.ts.map +1 -0
  42. package/dist/cloud/marketplace.js +159 -0
  43. package/dist/cloud/marketplace.js.map +1 -0
  44. package/dist/cloud/provisioning.d.ts +110 -0
  45. package/dist/cloud/provisioning.d.ts.map +1 -0
  46. package/dist/cloud/provisioning.js +148 -0
  47. package/dist/cloud/provisioning.js.map +1 -0
  48. package/dist/cloud/relay/endpoints.d.ts +62 -0
  49. package/dist/cloud/relay/endpoints.d.ts.map +1 -0
  50. package/dist/cloud/relay/endpoints.js +217 -0
  51. package/dist/cloud/relay/endpoints.js.map +1 -0
  52. package/dist/cloud/relay/health/index.d.ts +5 -0
  53. package/dist/cloud/relay/health/index.d.ts.map +1 -0
  54. package/dist/cloud/relay/health/index.js +9 -0
  55. package/dist/cloud/relay/health/index.js.map +1 -0
  56. package/dist/cloud/relay/stats/index.d.ts +5 -0
  57. package/dist/cloud/relay/stats/index.d.ts.map +1 -0
  58. package/dist/cloud/relay/stats/index.js +9 -0
  59. package/dist/cloud/relay/stats/index.js.map +1 -0
  60. package/dist/cloud/relay/sync/index.d.ts +5 -0
  61. package/dist/cloud/relay/sync/index.d.ts.map +1 -0
  62. package/dist/cloud/relay/sync/index.js +9 -0
  63. package/dist/cloud/relay/sync/index.js.map +1 -0
  64. package/dist/cloud/relay/usage/index.d.ts +5 -0
  65. package/dist/cloud/relay/usage/index.d.ts.map +1 -0
  66. package/dist/cloud/relay/usage/index.js +9 -0
  67. package/dist/cloud/relay/usage/index.js.map +1 -0
  68. package/dist/cloud/sponsors.d.ts +81 -0
  69. package/dist/cloud/sponsors.d.ts.map +1 -0
  70. package/dist/cloud/sponsors.js +130 -0
  71. package/dist/cloud/sponsors.js.map +1 -0
  72. package/dist/cloud/types.d.ts +169 -0
  73. package/dist/cloud/types.d.ts.map +1 -0
  74. package/dist/cloud/types.js +7 -0
  75. package/dist/cloud/types.js.map +1 -0
  76. package/dist/components/index.d.ts +43 -0
  77. package/dist/components/index.d.ts.map +1 -0
  78. package/dist/components/index.js +17 -0
  79. package/dist/components/index.js.map +1 -0
  80. package/dist/core/actors.d.ts +95 -0
  81. package/dist/core/actors.d.ts.map +1 -0
  82. package/dist/core/actors.js +158 -0
  83. package/dist/core/actors.js.map +1 -0
  84. package/dist/core/component/generator.d.ts +122 -0
  85. package/dist/core/component/generator.d.ts.map +1 -0
  86. package/dist/core/component/generator.js +307 -0
  87. package/dist/core/component/generator.js.map +1 -0
  88. package/dist/core/engine.d.ts +92 -0
  89. package/dist/core/engine.d.ts.map +1 -0
  90. package/dist/core/engine.js +199 -0
  91. package/dist/core/engine.js.map +1 -0
  92. package/dist/core/introspection.d.ts +141 -0
  93. package/dist/core/introspection.d.ts.map +1 -0
  94. package/dist/core/introspection.js +208 -0
  95. package/dist/core/introspection.js.map +1 -0
  96. package/dist/core/logic/generator.d.ts +76 -0
  97. package/dist/core/logic/generator.d.ts.map +1 -0
  98. package/dist/core/logic/generator.js +339 -0
  99. package/dist/core/logic/generator.js.map +1 -0
  100. package/dist/core/pluresdb/generator.d.ts +58 -0
  101. package/dist/core/pluresdb/generator.d.ts.map +1 -0
  102. package/dist/core/pluresdb/generator.js +162 -0
  103. package/dist/core/pluresdb/generator.js.map +1 -0
  104. package/dist/core/protocol.d.ts +121 -0
  105. package/dist/core/protocol.d.ts.map +1 -0
  106. package/dist/core/protocol.js +46 -0
  107. package/dist/core/protocol.js.map +1 -0
  108. package/dist/core/rules.d.ts +120 -0
  109. package/dist/core/rules.d.ts.map +1 -0
  110. package/dist/core/rules.js +81 -0
  111. package/dist/core/rules.js.map +1 -0
  112. package/dist/core/schema/loader.d.ts +47 -0
  113. package/dist/core/schema/loader.d.ts.map +1 -0
  114. package/dist/core/schema/loader.js +189 -0
  115. package/dist/core/schema/loader.js.map +1 -0
  116. package/dist/core/schema/normalize.d.ts +72 -0
  117. package/dist/core/schema/normalize.d.ts.map +1 -0
  118. package/dist/core/schema/normalize.js +190 -0
  119. package/dist/core/schema/normalize.js.map +1 -0
  120. package/dist/core/schema/types.d.ts +370 -0
  121. package/dist/core/schema/types.d.ts.map +1 -0
  122. package/dist/core/schema/types.js +161 -0
  123. package/dist/core/schema/types.js.map +1 -0
  124. package/dist/dsl/index.d.ts +152 -0
  125. package/dist/dsl/index.d.ts.map +1 -0
  126. package/dist/dsl/index.js +132 -0
  127. package/dist/dsl/index.js.map +1 -0
  128. package/dist/dsl.d.ts +124 -0
  129. package/dist/dsl.d.ts.map +1 -0
  130. package/dist/dsl.js +130 -0
  131. package/dist/dsl.js.map +1 -0
  132. package/dist/examples/advanced-todo/index.d.ts +55 -0
  133. package/dist/examples/advanced-todo/index.d.ts.map +1 -0
  134. package/dist/examples/advanced-todo/index.js +222 -0
  135. package/dist/examples/advanced-todo/index.js.map +1 -0
  136. package/dist/examples/auth-basic/index.d.ts +17 -0
  137. package/dist/examples/auth-basic/index.d.ts.map +1 -0
  138. package/dist/examples/auth-basic/index.js +122 -0
  139. package/dist/examples/auth-basic/index.js.map +1 -0
  140. package/dist/examples/cart/index.d.ts +19 -0
  141. package/dist/examples/cart/index.d.ts.map +1 -0
  142. package/dist/examples/cart/index.js +202 -0
  143. package/dist/examples/cart/index.js.map +1 -0
  144. package/dist/examples/hero-ecommerce/index.d.ts +39 -0
  145. package/dist/examples/hero-ecommerce/index.d.ts.map +1 -0
  146. package/dist/examples/hero-ecommerce/index.js +506 -0
  147. package/dist/examples/hero-ecommerce/index.js.map +1 -0
  148. package/dist/examples/svelte-counter/index.d.ts +31 -0
  149. package/dist/examples/svelte-counter/index.d.ts.map +1 -0
  150. package/dist/examples/svelte-counter/index.js +123 -0
  151. package/dist/examples/svelte-counter/index.js.map +1 -0
  152. package/dist/flows.d.ts +125 -0
  153. package/dist/flows.d.ts.map +1 -0
  154. package/dist/flows.js +160 -0
  155. package/dist/flows.js.map +1 -0
  156. package/dist/index.d.ts +67 -0
  157. package/dist/index.d.ts.map +1 -0
  158. package/dist/index.js +59 -0
  159. package/dist/index.js.map +1 -0
  160. package/dist/integrations/pluresdb.d.ts +56 -0
  161. package/dist/integrations/pluresdb.d.ts.map +1 -0
  162. package/dist/integrations/pluresdb.js +46 -0
  163. package/dist/integrations/pluresdb.js.map +1 -0
  164. package/dist/integrations/svelte.d.ts +306 -0
  165. package/dist/integrations/svelte.d.ts.map +1 -0
  166. package/dist/integrations/svelte.js +447 -0
  167. package/dist/integrations/svelte.js.map +1 -0
  168. package/dist/registry.d.ts +94 -0
  169. package/dist/registry.d.ts.map +1 -0
  170. package/dist/registry.js +181 -0
  171. package/dist/registry.js.map +1 -0
  172. package/dist/runtime/terminal-adapter.d.ts +105 -0
  173. package/dist/runtime/terminal-adapter.d.ts.map +1 -0
  174. package/dist/runtime/terminal-adapter.js +113 -0
  175. package/dist/runtime/terminal-adapter.js.map +1 -0
  176. package/dist/step.d.ts +34 -0
  177. package/dist/step.d.ts.map +1 -0
  178. package/dist/step.js +111 -0
  179. package/dist/step.js.map +1 -0
  180. package/dist/types.d.ts +63 -0
  181. package/dist/types.d.ts.map +1 -0
  182. package/dist/types.js +6 -0
  183. package/dist/types.js.map +1 -0
  184. package/docs/MONETIZATION.md +394 -0
  185. package/docs/TERMINAL_NODE.md +588 -0
  186. package/docs/guides/canvas.md +389 -0
  187. package/docs/guides/getting-started.md +347 -0
  188. package/docs/guides/history-state-pattern.md +618 -0
  189. package/docs/guides/orchestration.md +617 -0
  190. package/docs/guides/parallel-state-pattern.md +767 -0
  191. package/docs/guides/svelte-integration.md +691 -0
  192. package/package.json +96 -0
  193. package/src/__tests__/actors.test.ts +270 -0
  194. package/src/__tests__/billing.test.ts +175 -0
  195. package/src/__tests__/cloud.test.ts +247 -0
  196. package/src/__tests__/dsl.test.ts +154 -0
  197. package/src/__tests__/edge-cases.test.ts +475 -0
  198. package/src/__tests__/engine.test.ts +137 -0
  199. package/src/__tests__/generators.test.ts +270 -0
  200. package/src/__tests__/introspection.test.ts +321 -0
  201. package/src/__tests__/protocol.test.ts +40 -0
  202. package/src/__tests__/provisioning.test.ts +162 -0
  203. package/src/__tests__/schema.test.ts +241 -0
  204. package/src/__tests__/svelte-integration.test.ts +431 -0
  205. package/src/__tests__/terminal-node.test.ts +352 -0
  206. package/src/adapters/cli.ts +175 -0
  207. package/src/cli/commands/auth.ts +271 -0
  208. package/src/cli/commands/cloud.ts +281 -0
  209. package/src/cli/commands/generate.ts +225 -0
  210. package/src/cli/index.ts +190 -0
  211. package/src/cloud/README.md +383 -0
  212. package/src/cloud/auth.ts +245 -0
  213. package/src/cloud/billing.ts +336 -0
  214. package/src/cloud/client.ts +221 -0
  215. package/src/cloud/index.ts +121 -0
  216. package/src/cloud/marketplace.ts +303 -0
  217. package/src/cloud/provisioning.ts +254 -0
  218. package/src/cloud/relay/endpoints.ts +307 -0
  219. package/src/cloud/relay/health/function.json +17 -0
  220. package/src/cloud/relay/health/index.ts +10 -0
  221. package/src/cloud/relay/host.json +15 -0
  222. package/src/cloud/relay/local.settings.json +8 -0
  223. package/src/cloud/relay/stats/function.json +17 -0
  224. package/src/cloud/relay/stats/index.ts +10 -0
  225. package/src/cloud/relay/sync/function.json +17 -0
  226. package/src/cloud/relay/sync/index.ts +10 -0
  227. package/src/cloud/relay/usage/function.json +17 -0
  228. package/src/cloud/relay/usage/index.ts +10 -0
  229. package/src/cloud/sponsors.ts +213 -0
  230. package/src/cloud/types.ts +198 -0
  231. package/src/components/README.md +125 -0
  232. package/src/components/TerminalNode.svelte +457 -0
  233. package/src/components/index.ts +46 -0
  234. package/src/core/actors.ts +205 -0
  235. package/src/core/component/generator.ts +432 -0
  236. package/src/core/engine.ts +243 -0
  237. package/src/core/introspection.ts +329 -0
  238. package/src/core/logic/generator.ts +420 -0
  239. package/src/core/pluresdb/generator.ts +229 -0
  240. package/src/core/protocol.ts +132 -0
  241. package/src/core/rules.ts +167 -0
  242. package/src/core/schema/loader.ts +247 -0
  243. package/src/core/schema/normalize.ts +322 -0
  244. package/src/core/schema/types.ts +557 -0
  245. package/src/dsl/index.ts +218 -0
  246. package/src/dsl.ts +214 -0
  247. package/src/examples/advanced-todo/App.svelte +506 -0
  248. package/src/examples/advanced-todo/README.md +371 -0
  249. package/src/examples/advanced-todo/index.ts +309 -0
  250. package/src/examples/auth-basic/index.ts +163 -0
  251. package/src/examples/cart/index.ts +259 -0
  252. package/src/examples/hero-ecommerce/index.ts +657 -0
  253. package/src/examples/svelte-counter/index.ts +168 -0
  254. package/src/flows.ts +268 -0
  255. package/src/index.ts +154 -0
  256. package/src/integrations/pluresdb.ts +93 -0
  257. package/src/integrations/svelte.ts +617 -0
  258. package/src/registry.ts +223 -0
  259. package/src/runtime/terminal-adapter.ts +175 -0
  260. package/src/step.ts +151 -0
  261. package/src/types.ts +70 -0
  262. package/templates/basic-app/README.md +147 -0
  263. package/templates/fullstack-app/README.md +279 -0
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Tests for Svelte 5 Integration
3
+ *
4
+ * Tests the enhanced Svelte integration with runes support,
5
+ * history state pattern, and snapshot functionality.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach } from 'vitest';
9
+ import {
10
+ createPraxisEngine,
11
+ PraxisRegistry,
12
+ defineFact,
13
+ defineEvent,
14
+ defineRule,
15
+ findEvent,
16
+ } from '../index.js';
17
+ import {
18
+ createPraxisStore,
19
+ createContextStore,
20
+ createDerivedStore,
21
+ usePraxisEngine,
22
+ usePraxisContext,
23
+ HistoryStateManager,
24
+ createHistoryEngine,
25
+ } from '../integrations/svelte.js';
26
+
27
+ // Test context and events
28
+ interface CounterContext {
29
+ count: number;
30
+ history: number[];
31
+ }
32
+
33
+ const CountIncremented = defineFact<'CountIncremented', { amount: number }>('CountIncremented');
34
+ const Increment = defineEvent<'INCREMENT', { amount?: number }>('INCREMENT');
35
+
36
+ function createTestEngine() {
37
+ const registry = new PraxisRegistry<CounterContext>();
38
+
39
+ registry.registerRule(
40
+ defineRule<CounterContext>({
41
+ id: 'counter.increment',
42
+ description: 'Increment counter',
43
+ impl: (state, events) => {
44
+ const event = findEvent(events, Increment);
45
+ if (!event) return [];
46
+
47
+ const amount = event.payload.amount ?? 1;
48
+ state.context.count += amount;
49
+ state.context.history.push(state.context.count);
50
+
51
+ return [CountIncremented.create({ amount })];
52
+ },
53
+ })
54
+ );
55
+
56
+ return createPraxisEngine<CounterContext>({
57
+ initialContext: {
58
+ count: 0,
59
+ history: [0],
60
+ },
61
+ registry,
62
+ });
63
+ }
64
+
65
+ describe('Svelte Integration - Store API', () => {
66
+ it('should create a reactive Praxis store', () => {
67
+ const engine = createTestEngine();
68
+ const store = createPraxisStore(engine);
69
+
70
+ const states: any[] = [];
71
+ const unsubscribe = store.subscribe((state) => {
72
+ states.push(state);
73
+ });
74
+
75
+ // Initial state
76
+ expect(states.length).toBe(1);
77
+ expect(states[0].context.count).toBe(0);
78
+
79
+ // Dispatch event
80
+ store.dispatch([Increment.create({ amount: 5 })]);
81
+
82
+ expect(states.length).toBe(2);
83
+ expect(states[1].context.count).toBe(5);
84
+
85
+ unsubscribe();
86
+ });
87
+
88
+ it('should create a context store', () => {
89
+ const engine = createTestEngine();
90
+ const store = createContextStore(engine);
91
+
92
+ const contexts: any[] = [];
93
+ const unsubscribe = store.subscribe((ctx) => {
94
+ contexts.push(ctx);
95
+ });
96
+
97
+ expect(contexts.length).toBe(1);
98
+ expect(contexts[0].count).toBe(0);
99
+
100
+ store.dispatch([Increment.create({ amount: 3 })]);
101
+
102
+ expect(contexts.length).toBe(2);
103
+ expect(contexts[1].count).toBe(3);
104
+
105
+ unsubscribe();
106
+ });
107
+
108
+ it('should create a derived store with selector', () => {
109
+ const engine = createTestEngine();
110
+ const countStore = createDerivedStore(engine, (ctx: CounterContext) => ctx.count);
111
+
112
+ const counts: number[] = [];
113
+ const unsubscribe = countStore.subscribe((count) => {
114
+ counts.push(count);
115
+ });
116
+
117
+ expect(counts).toEqual([0]);
118
+
119
+ countStore.dispatch([Increment.create({ amount: 1 })]);
120
+ expect(counts).toEqual([0, 1]);
121
+
122
+ countStore.dispatch([Increment.create({ amount: 2 })]);
123
+ expect(counts).toEqual([0, 1, 3]);
124
+
125
+ unsubscribe();
126
+ });
127
+
128
+ it('should only notify on actual value changes in derived store', () => {
129
+ const engine = createTestEngine();
130
+ const countStore = createDerivedStore(engine, (ctx: CounterContext) => {
131
+ // Return same value regardless of actual count
132
+ return ctx.count > 0 ? 'positive' : 'zero';
133
+ });
134
+
135
+ const values: string[] = [];
136
+ const unsubscribe = countStore.subscribe((value) => {
137
+ values.push(value);
138
+ });
139
+
140
+ expect(values).toEqual(['zero']);
141
+
142
+ // First increment - should notify
143
+ countStore.dispatch([Increment.create({ amount: 1 })]);
144
+ expect(values).toEqual(['zero', 'positive']);
145
+
146
+ // Second increment - should NOT notify (value didn't change)
147
+ countStore.dispatch([Increment.create({ amount: 5 })]);
148
+ expect(values).toEqual(['zero', 'positive']);
149
+
150
+ unsubscribe();
151
+ });
152
+ });
153
+
154
+ describe('Svelte Integration - Runes API', () => {
155
+ it('should create engine binding with usePraxisEngine', () => {
156
+ const engine = createTestEngine();
157
+ const binding = usePraxisEngine(engine);
158
+
159
+ expect(binding.state.context.count).toBe(0);
160
+ expect(binding.context.count).toBe(0);
161
+ expect(binding.facts.length).toBe(0);
162
+
163
+ binding.dispatch([Increment.create({ amount: 7 })]);
164
+
165
+ expect(binding.context.count).toBe(7);
166
+ expect(binding.facts.length).toBe(1);
167
+ expect(binding.facts[0].tag).toBe('CountIncremented');
168
+ });
169
+
170
+ it('should support history with usePraxisEngine', () => {
171
+ const engine = createTestEngine();
172
+ const binding = usePraxisEngine(engine, { enableHistory: true });
173
+
174
+ expect(binding.canUndo).toBe(false);
175
+ expect(binding.canRedo).toBe(false);
176
+ expect(binding.snapshots.length).toBe(1);
177
+ expect(binding.historyIndex).toBe(0);
178
+
179
+ // First action
180
+ binding.dispatch([Increment.create({ amount: 1 })]);
181
+ expect(binding.context.count).toBe(1);
182
+ expect(binding.snapshots.length).toBe(2);
183
+ expect(binding.canUndo).toBe(true);
184
+ expect(binding.canRedo).toBe(false);
185
+
186
+ // Second action
187
+ binding.dispatch([Increment.create({ amount: 2 })]);
188
+ expect(binding.context.count).toBe(3);
189
+ expect(binding.snapshots.length).toBe(3);
190
+
191
+ // Undo
192
+ binding.undo();
193
+ expect(binding.context.count).toBe(1);
194
+ expect(binding.canUndo).toBe(true);
195
+ expect(binding.canRedo).toBe(true);
196
+
197
+ // Undo again
198
+ binding.undo();
199
+ expect(binding.context.count).toBe(0);
200
+ expect(binding.canUndo).toBe(false);
201
+ expect(binding.canRedo).toBe(true);
202
+
203
+ // Redo
204
+ binding.redo();
205
+ expect(binding.context.count).toBe(1);
206
+ expect(binding.canUndo).toBe(true);
207
+ expect(binding.canRedo).toBe(true);
208
+ });
209
+
210
+ it('should truncate future history when dispatching from past state', () => {
211
+ const engine = createTestEngine();
212
+ const binding = usePraxisEngine(engine, { enableHistory: true });
213
+
214
+ // Create history: 0 -> 1 -> 3 -> 6
215
+ binding.dispatch([Increment.create({ amount: 1 })]);
216
+ binding.dispatch([Increment.create({ amount: 2 })]);
217
+ binding.dispatch([Increment.create({ amount: 3 })]);
218
+ expect(binding.snapshots.length).toBe(4);
219
+
220
+ // Go back to count = 1
221
+ binding.undo();
222
+ binding.undo();
223
+ expect(binding.context.count).toBe(1);
224
+
225
+ // Dispatch new action - should truncate future history
226
+ // Note: The underlying engine state is at 6, but our snapshot shows 1
227
+ // When we dispatch, it applies to the actual engine state (6 + 10 = 16)
228
+ binding.dispatch([Increment.create({ amount: 10 })]);
229
+ expect(binding.context.count).toBe(16); // 6 + 10
230
+ expect(binding.snapshots.length).toBe(3); // Initial, +1, +10
231
+ expect(binding.canRedo).toBe(false);
232
+ });
233
+
234
+ it('should limit history size', () => {
235
+ const engine = createTestEngine();
236
+ const binding = usePraxisEngine(engine, {
237
+ enableHistory: true,
238
+ maxHistorySize: 3,
239
+ });
240
+
241
+ // Create more history entries than max size
242
+ binding.dispatch([Increment.create({ amount: 1 })]);
243
+ binding.dispatch([Increment.create({ amount: 1 })]);
244
+ binding.dispatch([Increment.create({ amount: 1 })]);
245
+ binding.dispatch([Increment.create({ amount: 1 })]);
246
+
247
+ expect(binding.snapshots.length).toBe(3); // Should be limited to max size
248
+ });
249
+
250
+ it('should extract context with usePraxisContext', () => {
251
+ const engine = createTestEngine();
252
+ const count = usePraxisContext(engine, (ctx: CounterContext) => ctx.count);
253
+
254
+ expect(count).toBe(0);
255
+ });
256
+ });
257
+
258
+ describe('History State Manager', () => {
259
+ it('should record and navigate history', () => {
260
+ const manager = new HistoryStateManager<CounterContext>(10);
261
+
262
+ // Record initial state
263
+ const state1 = {
264
+ context: { count: 0, history: [0] },
265
+ facts: [],
266
+ meta: {},
267
+ protocolVersion: '1.0.0',
268
+ };
269
+ manager.record(state1, [], 'Initial');
270
+
271
+ expect(manager.getCurrentIndex()).toBe(0);
272
+ expect(manager.canGoBack()).toBe(false);
273
+ expect(manager.canGoForward()).toBe(false);
274
+
275
+ // Record second state
276
+ const state2 = {
277
+ ...state1,
278
+ context: { count: 1, history: [0, 1] },
279
+ };
280
+ manager.record(state2, [Increment.create({ amount: 1 })], 'Increment');
281
+
282
+ expect(manager.getCurrentIndex()).toBe(1);
283
+ expect(manager.canGoBack()).toBe(true);
284
+ expect(manager.canGoForward()).toBe(false);
285
+
286
+ // Go back
287
+ const entry = manager.back();
288
+ expect(entry).not.toBeNull();
289
+ expect(entry?.state.context.count).toBe(0);
290
+ expect(manager.getCurrentIndex()).toBe(0);
291
+ expect(manager.canGoBack()).toBe(false);
292
+ expect(manager.canGoForward()).toBe(true);
293
+
294
+ // Go forward
295
+ const forwardEntry = manager.forward();
296
+ expect(forwardEntry).not.toBeNull();
297
+ expect(forwardEntry?.state.context.count).toBe(1);
298
+ expect(manager.getCurrentIndex()).toBe(1);
299
+ });
300
+
301
+ it('should truncate future history when recording from past', () => {
302
+ const manager = new HistoryStateManager<CounterContext>(10);
303
+
304
+ const state1 = {
305
+ context: { count: 0, history: [0] },
306
+ facts: [],
307
+ meta: {},
308
+ protocolVersion: '1.0.0',
309
+ };
310
+
311
+ // Record 3 states
312
+ manager.record(state1, []);
313
+ manager.record({ ...state1, context: { count: 1, history: [0, 1] } }, []);
314
+ manager.record({ ...state1, context: { count: 2, history: [0, 1, 2] } }, []);
315
+
316
+ expect(manager.getHistory().length).toBe(3);
317
+
318
+ // Go back to first state
319
+ manager.back();
320
+ manager.back();
321
+ expect(manager.getCurrentIndex()).toBe(0);
322
+
323
+ // Record new state - should truncate
324
+ manager.record({ ...state1, context: { count: 10, history: [0, 10] } }, []);
325
+
326
+ expect(manager.getHistory().length).toBe(2);
327
+ expect(manager.getCurrentIndex()).toBe(1);
328
+ });
329
+
330
+ it('should limit history size', () => {
331
+ const manager = new HistoryStateManager<CounterContext>(3);
332
+
333
+ const state = {
334
+ context: { count: 0, history: [0] },
335
+ facts: [],
336
+ meta: {},
337
+ protocolVersion: '1.0.0',
338
+ };
339
+
340
+ // Record 5 states
341
+ for (let i = 0; i < 5; i++) {
342
+ manager.record(
343
+ { ...state, context: { count: i, history: [i] } },
344
+ []
345
+ );
346
+ }
347
+
348
+ // Should only keep last 3
349
+ expect(manager.getHistory().length).toBe(3);
350
+ expect(manager.current()?.state.context.count).toBe(4);
351
+ });
352
+
353
+ it('should clear history', () => {
354
+ const manager = new HistoryStateManager<CounterContext>(10);
355
+
356
+ const state = {
357
+ context: { count: 0, history: [0] },
358
+ facts: [],
359
+ meta: {},
360
+ protocolVersion: '1.0.0',
361
+ };
362
+
363
+ manager.record(state, []);
364
+ manager.record({ ...state, context: { count: 1, history: [0, 1] } }, []);
365
+
366
+ expect(manager.getHistory().length).toBe(2);
367
+
368
+ manager.clear();
369
+
370
+ expect(manager.getHistory().length).toBe(0);
371
+ expect(manager.getCurrentIndex()).toBe(-1);
372
+ expect(manager.current()).toBeNull();
373
+ });
374
+ });
375
+
376
+ describe('History Engine', () => {
377
+ it('should create history engine with undo/redo', () => {
378
+ const baseEngine = createTestEngine();
379
+ const historyEngine = createHistoryEngine(baseEngine);
380
+
381
+ expect(historyEngine.canUndo()).toBe(false);
382
+ expect(historyEngine.canRedo()).toBe(false);
383
+
384
+ // Dispatch action
385
+ historyEngine.dispatch([Increment.create({ amount: 5 })], 'Add 5');
386
+ expect(baseEngine.getContext().count).toBe(5);
387
+ expect(historyEngine.canUndo()).toBe(true);
388
+
389
+ // Get history
390
+ const history = historyEngine.getHistory();
391
+ expect(history.length).toBe(2); // Initial + one action
392
+ expect(history[0].label).toBe('Initial');
393
+ expect(history[1].label).toBe('Add 5');
394
+ });
395
+
396
+ it('should provide history navigation', () => {
397
+ const baseEngine = createTestEngine();
398
+ const historyEngine = createHistoryEngine(baseEngine, {
399
+ maxHistorySize: 5,
400
+ initialLabel: 'Start',
401
+ });
402
+
403
+ historyEngine.dispatch([Increment.create({ amount: 1 })], 'First');
404
+ historyEngine.dispatch([Increment.create({ amount: 2 })], 'Second');
405
+ historyEngine.dispatch([Increment.create({ amount: 3 })], 'Third');
406
+
407
+ const history = historyEngine.getHistory();
408
+ expect(history.length).toBe(4); // Start + 3 actions
409
+ expect(history[0].label).toBe('Start');
410
+ expect(history[1].label).toBe('First');
411
+ expect(history[2].label).toBe('Second');
412
+ expect(history[3].label).toBe('Third');
413
+
414
+ // Navigate to specific point
415
+ const success = historyEngine.goToHistory(1);
416
+ expect(success).toBe(true);
417
+ });
418
+
419
+ it('should clear history', () => {
420
+ const baseEngine = createTestEngine();
421
+ const historyEngine = createHistoryEngine(baseEngine);
422
+
423
+ historyEngine.dispatch([Increment.create({ amount: 1 })]);
424
+ historyEngine.dispatch([Increment.create({ amount: 2 })]);
425
+
426
+ expect(historyEngine.getHistory().length).toBe(3);
427
+
428
+ historyEngine.clearHistory();
429
+ expect(historyEngine.getHistory().length).toBe(0);
430
+ });
431
+ });