@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
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@plures/praxis",
3
+ "version": "0.2.0",
4
+ "description": "The Full Plures Application Framework - declarative schemas, logic engine, component generation, and local-first data",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "praxis": "./dist/cli/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./svelte": {
17
+ "types": "./dist/integrations/svelte.d.ts",
18
+ "default": "./dist/integrations/svelte.js"
19
+ },
20
+ "./schema": {
21
+ "types": "./dist/core/schema/types.d.ts",
22
+ "default": "./dist/core/schema/types.js"
23
+ },
24
+ "./component": {
25
+ "types": "./dist/core/component/generator.d.ts",
26
+ "default": "./dist/core/component/generator.js"
27
+ },
28
+ "./cloud": {
29
+ "types": "./dist/cloud/index.d.ts",
30
+ "default": "./dist/cloud/index.js"
31
+ },
32
+ "./components": {
33
+ "types": "./dist/components/index.d.ts",
34
+ "svelte": "./src/components/TerminalNode.svelte"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "core",
41
+ "cli",
42
+ "templates",
43
+ "docs",
44
+ "README.md",
45
+ "FRAMEWORK.md",
46
+ "src/components"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "test:ui": "vitest --ui",
53
+ "typecheck": "tsc --noEmit",
54
+ "cli": "node ./dist/cli/index.js"
55
+ },
56
+ "keywords": [
57
+ "praxis",
58
+ "framework",
59
+ "plures",
60
+ "logic",
61
+ "schema",
62
+ "component-generation",
63
+ "local-first",
64
+ "functional",
65
+ "typescript",
66
+ "state-management",
67
+ "facts",
68
+ "rules",
69
+ "constraints",
70
+ "orchestration",
71
+ "canvas",
72
+ "visual-development"
73
+ ],
74
+ "author": "Plures",
75
+ "license": "MIT",
76
+ "devDependencies": {
77
+ "@types/js-yaml": "^4.0.9",
78
+ "@types/node": "^24.10.1",
79
+ "@vitest/ui": "^4.0.10",
80
+ "tsx": "^4.20.6",
81
+ "typescript": "^5.9.3",
82
+ "vitest": "^4.0.10"
83
+ },
84
+ "dependencies": {
85
+ "commander": "^14.0.2",
86
+ "js-yaml": "^4.1.1"
87
+ },
88
+ "peerDependencies": {
89
+ "svelte": "^5.0.0"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "svelte": {
93
+ "optional": true
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Actor system tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { ActorManager, createTimerActor, type Actor } from "../core/actors.js";
7
+ import { createPraxisEngine } from "../core/engine.js";
8
+ import { PraxisRegistry } from "../core/rules.js";
9
+ import { defineEvent, defineRule, defineFact } from "../dsl/index.js";
10
+
11
+ describe("Actor System", () => {
12
+ describe("ActorManager", () => {
13
+ let manager: ActorManager<{ count: number }>;
14
+ let registry: PraxisRegistry<{ count: number }>;
15
+ let engine: ReturnType<typeof createPraxisEngine<{ count: number }>>;
16
+
17
+ beforeEach(() => {
18
+ manager = new ActorManager();
19
+ registry = new PraxisRegistry();
20
+ engine = createPraxisEngine({
21
+ initialContext: { count: 0 },
22
+ registry,
23
+ });
24
+ manager.attachEngine(engine);
25
+ });
26
+
27
+ it("should register and start an actor", async () => {
28
+ const onStartSpy = vi.fn();
29
+ const actor: Actor<{ count: number }> = {
30
+ id: "test-actor",
31
+ description: "Test actor",
32
+ onStart: onStartSpy,
33
+ };
34
+
35
+ manager.register(actor);
36
+ await manager.start("test-actor");
37
+
38
+ expect(onStartSpy).toHaveBeenCalledWith(engine);
39
+ expect(manager.isActive("test-actor")).toBe(true);
40
+ });
41
+
42
+ it("should throw when registering duplicate actor IDs", () => {
43
+ const actor: Actor<{ count: number }> = {
44
+ id: "duplicate",
45
+ description: "Test actor",
46
+ };
47
+
48
+ manager.register(actor);
49
+ expect(() => manager.register(actor)).toThrow('Actor with id "duplicate" already registered');
50
+ });
51
+
52
+ it("should throw when starting already active actor", async () => {
53
+ const actor: Actor<{ count: number }> = {
54
+ id: "test",
55
+ description: "Test",
56
+ };
57
+
58
+ manager.register(actor);
59
+ await manager.start("test");
60
+ await expect(manager.start("test")).rejects.toThrow('Actor "test" is already started');
61
+ });
62
+
63
+ it("should stop an actor", async () => {
64
+ const onStopSpy = vi.fn();
65
+ const actor: Actor<{ count: number }> = {
66
+ id: "test-actor",
67
+ description: "Test actor",
68
+ onStop: onStopSpy,
69
+ };
70
+
71
+ manager.register(actor);
72
+ await manager.start("test-actor");
73
+ await manager.stop("test-actor");
74
+
75
+ expect(onStopSpy).toHaveBeenCalled();
76
+ expect(manager.isActive("test-actor")).toBe(false);
77
+ });
78
+
79
+ it("should notify actors of state changes", async () => {
80
+ const onStateChangeSpy = vi.fn();
81
+ const actor: Actor<{ count: number }> = {
82
+ id: "observer",
83
+ description: "Observer actor",
84
+ onStateChange: onStateChangeSpy,
85
+ };
86
+
87
+ manager.register(actor);
88
+ await manager.start("observer");
89
+
90
+ const state = engine.getState();
91
+ await manager.notifyStateChange(state);
92
+
93
+ expect(onStateChangeSpy).toHaveBeenCalledWith(state, engine);
94
+ });
95
+
96
+ it("should start all registered actors", async () => {
97
+ const onStart1 = vi.fn();
98
+ const onStart2 = vi.fn();
99
+
100
+ manager.register({ id: "actor1", description: "Actor 1", onStart: onStart1 });
101
+ manager.register({ id: "actor2", description: "Actor 2", onStart: onStart2 });
102
+
103
+ await manager.startAll();
104
+
105
+ expect(onStart1).toHaveBeenCalled();
106
+ expect(onStart2).toHaveBeenCalled();
107
+ expect(manager.getActiveActorIds()).toHaveLength(2);
108
+ });
109
+
110
+ it("should stop all active actors", async () => {
111
+ const onStop1 = vi.fn();
112
+ const onStop2 = vi.fn();
113
+
114
+ manager.register({ id: "actor1", description: "Actor 1", onStop: onStop1 });
115
+ manager.register({ id: "actor2", description: "Actor 2", onStop: onStop2 });
116
+
117
+ await manager.startAll();
118
+ await manager.stopAll();
119
+
120
+ expect(onStop1).toHaveBeenCalled();
121
+ expect(onStop2).toHaveBeenCalled();
122
+ expect(manager.getActiveActorIds()).toHaveLength(0);
123
+ });
124
+
125
+ it("should handle async actor methods", async () => {
126
+ let startResolved = false;
127
+ let stopResolved = false;
128
+
129
+ const actor: Actor<{ count: number }> = {
130
+ id: "async-actor",
131
+ description: "Async actor",
132
+ onStart: async () => {
133
+ await new Promise((resolve) => setTimeout(resolve, 10));
134
+ startResolved = true;
135
+ },
136
+ onStop: async () => {
137
+ await new Promise((resolve) => setTimeout(resolve, 10));
138
+ stopResolved = true;
139
+ },
140
+ };
141
+
142
+ manager.register(actor);
143
+ await manager.start("async-actor");
144
+ expect(startResolved).toBe(true);
145
+
146
+ await manager.stop("async-actor");
147
+ expect(stopResolved).toBe(true);
148
+ });
149
+
150
+ it("should not notify stopped actors of state changes", async () => {
151
+ const onStateChangeSpy = vi.fn();
152
+ const actor: Actor<{ count: number }> = {
153
+ id: "observer",
154
+ description: "Observer",
155
+ onStateChange: onStateChangeSpy,
156
+ };
157
+
158
+ manager.register(actor);
159
+ await manager.start("observer");
160
+ await manager.stop("observer");
161
+
162
+ const state = engine.getState();
163
+ await manager.notifyStateChange(state);
164
+
165
+ expect(onStateChangeSpy).not.toHaveBeenCalled();
166
+ });
167
+
168
+ it("should throw when starting actor without attached engine", async () => {
169
+ const actor: Actor<{ count: number }> = {
170
+ id: "test",
171
+ description: "Test",
172
+ };
173
+
174
+ const newManager = new ActorManager<{ count: number }>();
175
+ newManager.register(actor);
176
+
177
+ await expect(newManager.start("test")).rejects.toThrow(
178
+ "Actor manager not attached to an engine"
179
+ );
180
+ });
181
+ });
182
+
183
+ describe("createTimerActor", () => {
184
+ it("should create a timer actor that dispatches events", async () => {
185
+ vi.useFakeTimers();
186
+
187
+ const Tick = defineEvent<"TICK", {}>("TICK");
188
+ const TickReceived = defineFact<"TickReceived", {}>("TickReceived");
189
+
190
+ const tickRule = defineRule<{ count: number }>({
191
+ id: "tick.rule",
192
+ description: "Count ticks",
193
+ impl: (state, events) => {
194
+ if (events.some(Tick.is)) {
195
+ state.context.count += 1;
196
+ return [TickReceived.create({})];
197
+ }
198
+ return [];
199
+ },
200
+ });
201
+
202
+ const registry = new PraxisRegistry<{ count: number }>();
203
+ registry.registerRule(tickRule);
204
+
205
+ const engine = createPraxisEngine({
206
+ initialContext: { count: 0 },
207
+ registry,
208
+ });
209
+
210
+ const manager = new ActorManager<{ count: number }>();
211
+ manager.attachEngine(engine);
212
+
213
+ const timerActor = createTimerActor("timer", 100, () => Tick.create({}));
214
+ manager.register(timerActor);
215
+ await manager.start("timer");
216
+
217
+ // Advance timers
218
+ vi.advanceTimersByTime(250);
219
+
220
+ // Should have ticked at least twice (at 100ms and 200ms)
221
+ expect(engine.getContext().count).toBeGreaterThanOrEqual(2);
222
+
223
+ await manager.stop("timer");
224
+ vi.useRealTimers();
225
+ });
226
+
227
+ it("should stop dispatching events after actor is stopped", async () => {
228
+ vi.useFakeTimers();
229
+
230
+ const Tick = defineEvent<"TICK", {}>("TICK");
231
+ const registry = new PraxisRegistry<{ count: number }>();
232
+
233
+ const tickRule = defineRule<{ count: number }>({
234
+ id: "tick.rule",
235
+ description: "Count ticks",
236
+ impl: (state, events) => {
237
+ if (events.some(Tick.is)) {
238
+ state.context.count += 1;
239
+ }
240
+ return [];
241
+ },
242
+ });
243
+ registry.registerRule(tickRule);
244
+
245
+ const engine = createPraxisEngine({
246
+ initialContext: { count: 0 },
247
+ registry,
248
+ });
249
+
250
+ const manager = new ActorManager<{ count: number }>();
251
+ manager.attachEngine(engine);
252
+
253
+ const timerActor = createTimerActor("timer", 50, () => Tick.create({}));
254
+ manager.register(timerActor);
255
+ await manager.start("timer");
256
+
257
+ vi.advanceTimersByTime(100);
258
+ const countBeforeStop = engine.getContext().count;
259
+
260
+ await manager.stop("timer");
261
+ vi.advanceTimersByTime(200);
262
+ const countAfterStop = engine.getContext().count;
263
+
264
+ // Count should not increase after stopping
265
+ expect(countAfterStop).toBe(countBeforeStop);
266
+
267
+ vi.useRealTimers();
268
+ });
269
+ });
270
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Billing Tests
3
+ *
4
+ * Tests for billing types and utilities.
5
+ */
6
+
7
+ import { describe, it, expect } from "vitest";
8
+ import {
9
+ SubscriptionTier,
10
+ SubscriptionStatus,
11
+ BillingProvider,
12
+ TIER_LIMITS,
13
+ hasAccessToTier,
14
+ checkUsageLimits,
15
+ createFreeSubscription,
16
+ createSponsorSubscription,
17
+ } from "../cloud/billing.js";
18
+
19
+ describe("Billing", () => {
20
+ describe("Tier Limits", () => {
21
+ it("should have limits for all tiers", () => {
22
+ expect(TIER_LIMITS[SubscriptionTier.FREE]).toBeDefined();
23
+ expect(TIER_LIMITS[SubscriptionTier.SOLO]).toBeDefined();
24
+ expect(TIER_LIMITS[SubscriptionTier.TEAM]).toBeDefined();
25
+ expect(TIER_LIMITS[SubscriptionTier.ENTERPRISE]).toBeDefined();
26
+ });
27
+
28
+ it("should have increasing limits for higher tiers", () => {
29
+ expect(TIER_LIMITS[SubscriptionTier.SOLO].maxSyncsPerMonth).toBeGreaterThan(
30
+ TIER_LIMITS[SubscriptionTier.FREE].maxSyncsPerMonth
31
+ );
32
+ expect(TIER_LIMITS[SubscriptionTier.TEAM].maxSyncsPerMonth).toBeGreaterThan(
33
+ TIER_LIMITS[SubscriptionTier.SOLO].maxSyncsPerMonth
34
+ );
35
+ expect(TIER_LIMITS[SubscriptionTier.ENTERPRISE].maxSyncsPerMonth).toBeGreaterThan(
36
+ TIER_LIMITS[SubscriptionTier.TEAM].maxSyncsPerMonth
37
+ );
38
+ });
39
+ });
40
+
41
+ describe("hasAccessToTier", () => {
42
+ it("should grant access to same tier", () => {
43
+ const subscription = createFreeSubscription();
44
+ expect(hasAccessToTier(subscription, SubscriptionTier.FREE)).toBe(true);
45
+ });
46
+
47
+ it("should grant access to lower tiers", () => {
48
+ const subscription = {
49
+ ...createFreeSubscription(),
50
+ tier: SubscriptionTier.ENTERPRISE,
51
+ };
52
+ expect(hasAccessToTier(subscription, SubscriptionTier.FREE)).toBe(true);
53
+ expect(hasAccessToTier(subscription, SubscriptionTier.SOLO)).toBe(true);
54
+ expect(hasAccessToTier(subscription, SubscriptionTier.TEAM)).toBe(true);
55
+ });
56
+
57
+ it("should deny access to higher tiers", () => {
58
+ const subscription = createFreeSubscription();
59
+ expect(hasAccessToTier(subscription, SubscriptionTier.SOLO)).toBe(false);
60
+ expect(hasAccessToTier(subscription, SubscriptionTier.TEAM)).toBe(false);
61
+ expect(hasAccessToTier(subscription, SubscriptionTier.ENTERPRISE)).toBe(false);
62
+ });
63
+
64
+ it("should deny access if subscription is not active", () => {
65
+ const subscription = {
66
+ ...createFreeSubscription(),
67
+ status: SubscriptionStatus.EXPIRED,
68
+ };
69
+ expect(hasAccessToTier(subscription, SubscriptionTier.FREE)).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("checkUsageLimits", () => {
74
+ it("should pass when usage is within limits", () => {
75
+ const subscription = createFreeSubscription();
76
+ const result = checkUsageLimits(subscription, {
77
+ syncCount: 500,
78
+ storageBytes: 5 * 1024 * 1024, // 5 MB
79
+ teamMembers: 1,
80
+ appCount: 1,
81
+ });
82
+ expect(result.withinLimits).toBe(true);
83
+ expect(result.violations).toHaveLength(0);
84
+ });
85
+
86
+ it("should fail when sync count exceeds limit", () => {
87
+ const subscription = createFreeSubscription();
88
+ const result = checkUsageLimits(subscription, {
89
+ syncCount: 2000,
90
+ storageBytes: 0,
91
+ teamMembers: 1,
92
+ appCount: 1,
93
+ });
94
+ expect(result.withinLimits).toBe(false);
95
+ expect(result.violations.length).toBeGreaterThan(0);
96
+ expect(result.violations[0]).toContain("Sync limit exceeded");
97
+ });
98
+
99
+ it("should fail when storage exceeds limit", () => {
100
+ const subscription = createFreeSubscription();
101
+ const result = checkUsageLimits(subscription, {
102
+ syncCount: 0,
103
+ storageBytes: 20 * 1024 * 1024, // 20 MB
104
+ teamMembers: 1,
105
+ appCount: 1,
106
+ });
107
+ expect(result.withinLimits).toBe(false);
108
+ expect(result.violations.length).toBeGreaterThan(0);
109
+ expect(result.violations[0]).toContain("Storage limit exceeded");
110
+ });
111
+
112
+ it("should fail when team members exceed limit", () => {
113
+ const subscription = createFreeSubscription();
114
+ const result = checkUsageLimits(subscription, {
115
+ syncCount: 0,
116
+ storageBytes: 0,
117
+ teamMembers: 5,
118
+ appCount: 1,
119
+ });
120
+ expect(result.withinLimits).toBe(false);
121
+ expect(result.violations.length).toBeGreaterThan(0);
122
+ expect(result.violations[0]).toContain("Team member limit exceeded");
123
+ });
124
+
125
+ it("should allow unlimited team members for enterprise", () => {
126
+ const subscription = {
127
+ ...createFreeSubscription(),
128
+ tier: SubscriptionTier.ENTERPRISE,
129
+ limits: TIER_LIMITS[SubscriptionTier.ENTERPRISE],
130
+ };
131
+ const result = checkUsageLimits(subscription, {
132
+ syncCount: 0,
133
+ storageBytes: 0,
134
+ teamMembers: 1000,
135
+ appCount: 1,
136
+ });
137
+ expect(result.withinLimits).toBe(true);
138
+ });
139
+ });
140
+
141
+ describe("createFreeSubscription", () => {
142
+ it("should create a free subscription", () => {
143
+ const subscription = createFreeSubscription();
144
+ expect(subscription.tier).toBe(SubscriptionTier.FREE);
145
+ expect(subscription.status).toBe(SubscriptionStatus.ACTIVE);
146
+ expect(subscription.provider).toBe(BillingProvider.NONE);
147
+ expect(subscription.autoRenew).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe("createSponsorSubscription", () => {
152
+ it("should create solo tier for $5/month", () => {
153
+ const subscription = createSponsorSubscription("Solo", 500);
154
+ expect(subscription.tier).toBe(SubscriptionTier.SOLO);
155
+ expect(subscription.provider).toBe(BillingProvider.SPONSORS);
156
+ });
157
+
158
+ it("should create team tier for $20/month", () => {
159
+ const subscription = createSponsorSubscription("Team", 2000);
160
+ expect(subscription.tier).toBe(SubscriptionTier.TEAM);
161
+ expect(subscription.provider).toBe(BillingProvider.SPONSORS);
162
+ });
163
+
164
+ it("should create enterprise tier for $50/month", () => {
165
+ const subscription = createSponsorSubscription("Enterprise", 5000);
166
+ expect(subscription.tier).toBe(SubscriptionTier.ENTERPRISE);
167
+ expect(subscription.provider).toBe(BillingProvider.SPONSORS);
168
+ });
169
+
170
+ it("should default to free tier for low amounts", () => {
171
+ const subscription = createSponsorSubscription("Supporter", 100);
172
+ expect(subscription.tier).toBe(SubscriptionTier.FREE);
173
+ });
174
+ });
175
+ });