@digitalpaws/starr 0.0.1

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 (290) hide show
  1. package/README.rst +75 -0
  2. package/data/.nfs.811bbb0d.43c34.4 +0 -0
  3. package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
  4. package/data/.nfs.82d127f7.fffff5704.4 +0 -0
  5. package/data/.nfs.82d127fe.fffff5704.4 +0 -0
  6. package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
  7. package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
  8. package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
  9. package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
  10. package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
  11. package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
  12. package/data/.nfs.82d18536.fffff5e24.4 +0 -0
  13. package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
  14. package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
  15. package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
  16. package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
  17. package/data/.nfs.8314122e.fffffb044.4 +0 -0
  18. package/data/.nfs.83141236.fffffb044.4 +0 -0
  19. package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
  20. package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
  21. package/data/logs/starr.log +66244 -0
  22. package/data/starr.db +0 -0
  23. package/data/starr.db-shm +0 -0
  24. package/data/starr.db-wal +0 -0
  25. package/data/starr.jwt +1 -0
  26. package/data/starr.key +1 -0
  27. package/dist/auth/auth-service.d.ts +42 -0
  28. package/dist/auth/auth-service.d.ts.map +1 -0
  29. package/dist/auth/auth-service.js +226 -0
  30. package/dist/auth/auth-service.js.map +1 -0
  31. package/dist/auth/http-server.d.ts +16 -0
  32. package/dist/auth/http-server.d.ts.map +1 -0
  33. package/dist/auth/http-server.js +207 -0
  34. package/dist/auth/http-server.js.map +1 -0
  35. package/dist/auth/jwt-secret.d.ts +6 -0
  36. package/dist/auth/jwt-secret.d.ts.map +1 -0
  37. package/dist/auth/jwt-secret.js +51 -0
  38. package/dist/auth/jwt-secret.js.map +1 -0
  39. package/dist/auth/oidc/oidc-flow.d.ts +60 -0
  40. package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
  41. package/dist/auth/oidc/oidc-flow.js +201 -0
  42. package/dist/auth/oidc/oidc-flow.js.map +1 -0
  43. package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
  44. package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
  45. package/dist/auth/oidc/oidc-provider-service.js +124 -0
  46. package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
  47. package/dist/auth/oidc/user-service.d.ts +67 -0
  48. package/dist/auth/oidc/user-service.d.ts.map +1 -0
  49. package/dist/auth/oidc/user-service.js +177 -0
  50. package/dist/auth/oidc/user-service.js.map +1 -0
  51. package/dist/auth/rate-limiter.d.ts +31 -0
  52. package/dist/auth/rate-limiter.d.ts.map +1 -0
  53. package/dist/auth/rate-limiter.js +70 -0
  54. package/dist/auth/rate-limiter.js.map +1 -0
  55. package/dist/config/config-service.d.ts +83 -0
  56. package/dist/config/config-service.d.ts.map +1 -0
  57. package/dist/config/config-service.js +309 -0
  58. package/dist/config/config-service.js.map +1 -0
  59. package/dist/config/validate.d.ts +12 -0
  60. package/dist/config/validate.d.ts.map +1 -0
  61. package/dist/config/validate.js +140 -0
  62. package/dist/config/validate.js.map +1 -0
  63. package/dist/content-types/registry.d.ts +75 -0
  64. package/dist/content-types/registry.d.ts.map +1 -0
  65. package/dist/content-types/registry.js +171 -0
  66. package/dist/content-types/registry.js.map +1 -0
  67. package/dist/datastore/db.d.ts +9 -0
  68. package/dist/datastore/db.d.ts.map +1 -0
  69. package/dist/datastore/db.js +27 -0
  70. package/dist/datastore/db.js.map +1 -0
  71. package/dist/datastore/encrypt.d.ts +9 -0
  72. package/dist/datastore/encrypt.d.ts.map +1 -0
  73. package/dist/datastore/encrypt.js +84 -0
  74. package/dist/datastore/encrypt.js.map +1 -0
  75. package/dist/datastore/migrations.d.ts +10 -0
  76. package/dist/datastore/migrations.d.ts.map +1 -0
  77. package/dist/datastore/migrations.js +340 -0
  78. package/dist/datastore/migrations.js.map +1 -0
  79. package/dist/datastore/storage.d.ts +4 -0
  80. package/dist/datastore/storage.d.ts.map +1 -0
  81. package/dist/datastore/storage.js +60 -0
  82. package/dist/datastore/storage.js.map +1 -0
  83. package/dist/events/event-bus.d.ts +15 -0
  84. package/dist/events/event-bus.d.ts.map +1 -0
  85. package/dist/events/event-bus.js +140 -0
  86. package/dist/events/event-bus.js.map +1 -0
  87. package/dist/events/spine.d.ts +31 -0
  88. package/dist/events/spine.d.ts.map +1 -0
  89. package/dist/events/spine.js +68 -0
  90. package/dist/events/spine.js.map +1 -0
  91. package/dist/events/trace.d.ts +24 -0
  92. package/dist/events/trace.d.ts.map +1 -0
  93. package/dist/events/trace.js +33 -0
  94. package/dist/events/trace.js.map +1 -0
  95. package/dist/host.d.ts +65 -0
  96. package/dist/host.d.ts.map +1 -0
  97. package/dist/host.js +94 -0
  98. package/dist/host.js.map +1 -0
  99. package/dist/library/importer.d.ts +29 -0
  100. package/dist/library/importer.d.ts.map +1 -0
  101. package/dist/library/importer.js +145 -0
  102. package/dist/library/importer.js.map +1 -0
  103. package/dist/library/library-cache.d.ts +46 -0
  104. package/dist/library/library-cache.d.ts.map +1 -0
  105. package/dist/library/library-cache.js +152 -0
  106. package/dist/library/library-cache.js.map +1 -0
  107. package/dist/library/library-config.d.ts +12 -0
  108. package/dist/library/library-config.d.ts.map +1 -0
  109. package/dist/library/library-config.js +10 -0
  110. package/dist/library/library-config.js.map +1 -0
  111. package/dist/library/library-scanner.d.ts +62 -0
  112. package/dist/library/library-scanner.d.ts.map +1 -0
  113. package/dist/library/library-scanner.js +336 -0
  114. package/dist/library/library-scanner.js.map +1 -0
  115. package/dist/library/library-service.d.ts +65 -0
  116. package/dist/library/library-service.d.ts.map +1 -0
  117. package/dist/library/library-service.js +197 -0
  118. package/dist/library/library-service.js.map +1 -0
  119. package/dist/library/metadata-matcher.d.ts +56 -0
  120. package/dist/library/metadata-matcher.d.ts.map +1 -0
  121. package/dist/library/metadata-matcher.js +207 -0
  122. package/dist/library/metadata-matcher.js.map +1 -0
  123. package/dist/library/naming.d.ts +8 -0
  124. package/dist/library/naming.d.ts.map +1 -0
  125. package/dist/library/naming.js +47 -0
  126. package/dist/library/naming.js.map +1 -0
  127. package/dist/library/scan-controller.d.ts +95 -0
  128. package/dist/library/scan-controller.d.ts.map +1 -0
  129. package/dist/library/scan-controller.js +379 -0
  130. package/dist/library/scan-controller.js.map +1 -0
  131. package/dist/library/source-resolver.d.ts +17 -0
  132. package/dist/library/source-resolver.d.ts.map +1 -0
  133. package/dist/library/source-resolver.js +75 -0
  134. package/dist/library/source-resolver.js.map +1 -0
  135. package/dist/logging/logger.d.ts +3 -0
  136. package/dist/logging/logger.d.ts.map +1 -0
  137. package/dist/logging/logger.js +107 -0
  138. package/dist/logging/logger.js.map +1 -0
  139. package/dist/main.d.ts +36 -0
  140. package/dist/main.d.ts.map +1 -0
  141. package/dist/main.js +229 -0
  142. package/dist/main.js.map +1 -0
  143. package/dist/plugins/context.d.ts +10 -0
  144. package/dist/plugins/context.d.ts.map +1 -0
  145. package/dist/plugins/context.js +110 -0
  146. package/dist/plugins/context.js.map +1 -0
  147. package/dist/plugins/discovery.d.ts +2 -0
  148. package/dist/plugins/discovery.d.ts.map +1 -0
  149. package/dist/plugins/discovery.js +32 -0
  150. package/dist/plugins/discovery.js.map +1 -0
  151. package/dist/plugins/loader.d.ts +9 -0
  152. package/dist/plugins/loader.d.ts.map +1 -0
  153. package/dist/plugins/loader.js +245 -0
  154. package/dist/plugins/loader.js.map +1 -0
  155. package/dist/state/request-intake.d.ts +26 -0
  156. package/dist/state/request-intake.d.ts.map +1 -0
  157. package/dist/state/request-intake.js +74 -0
  158. package/dist/state/request-intake.js.map +1 -0
  159. package/dist/state/state-store.d.ts +146 -0
  160. package/dist/state/state-store.d.ts.map +1 -0
  161. package/dist/state/state-store.js +465 -0
  162. package/dist/state/state-store.js.map +1 -0
  163. package/dist/state/trace-store.d.ts +12 -0
  164. package/dist/state/trace-store.d.ts.map +1 -0
  165. package/dist/state/trace-store.js +24 -0
  166. package/dist/state/trace-store.js.map +1 -0
  167. package/dist/types/plugin.d.ts +93 -0
  168. package/dist/types/plugin.d.ts.map +1 -0
  169. package/dist/types/plugin.js +4 -0
  170. package/dist/types/plugin.js.map +1 -0
  171. package/dist/web/index.d.ts +10 -0
  172. package/dist/web/index.d.ts.map +1 -0
  173. package/dist/web/index.js +153 -0
  174. package/dist/web/index.js.map +1 -0
  175. package/dist/web/routes/activity.d.ts +6 -0
  176. package/dist/web/routes/activity.d.ts.map +1 -0
  177. package/dist/web/routes/activity.js +31 -0
  178. package/dist/web/routes/activity.js.map +1 -0
  179. package/dist/web/routes/auth.d.ts +6 -0
  180. package/dist/web/routes/auth.d.ts.map +1 -0
  181. package/dist/web/routes/auth.js +142 -0
  182. package/dist/web/routes/auth.js.map +1 -0
  183. package/dist/web/routes/config.d.ts +7 -0
  184. package/dist/web/routes/config.d.ts.map +1 -0
  185. package/dist/web/routes/config.js +204 -0
  186. package/dist/web/routes/config.js.map +1 -0
  187. package/dist/web/routes/content-types.d.ts +6 -0
  188. package/dist/web/routes/content-types.d.ts.map +1 -0
  189. package/dist/web/routes/content-types.js +29 -0
  190. package/dist/web/routes/content-types.js.map +1 -0
  191. package/dist/web/routes/discovery.d.ts +6 -0
  192. package/dist/web/routes/discovery.d.ts.map +1 -0
  193. package/dist/web/routes/discovery.js +111 -0
  194. package/dist/web/routes/discovery.js.map +1 -0
  195. package/dist/web/routes/library-import.d.ts +6 -0
  196. package/dist/web/routes/library-import.d.ts.map +1 -0
  197. package/dist/web/routes/library-import.js +120 -0
  198. package/dist/web/routes/library-import.js.map +1 -0
  199. package/dist/web/routes/library-management.d.ts +6 -0
  200. package/dist/web/routes/library-management.d.ts.map +1 -0
  201. package/dist/web/routes/library-management.js +120 -0
  202. package/dist/web/routes/library-management.js.map +1 -0
  203. package/dist/web/routes/library.d.ts +6 -0
  204. package/dist/web/routes/library.d.ts.map +1 -0
  205. package/dist/web/routes/library.js +28 -0
  206. package/dist/web/routes/library.js.map +1 -0
  207. package/dist/web/routes/oidc.d.ts +9 -0
  208. package/dist/web/routes/oidc.d.ts.map +1 -0
  209. package/dist/web/routes/oidc.js +203 -0
  210. package/dist/web/routes/oidc.js.map +1 -0
  211. package/dist/web/routes/requests.d.ts +6 -0
  212. package/dist/web/routes/requests.d.ts.map +1 -0
  213. package/dist/web/routes/requests.js +183 -0
  214. package/dist/web/routes/requests.js.map +1 -0
  215. package/dist/web/routes/sse.d.ts +6 -0
  216. package/dist/web/routes/sse.d.ts.map +1 -0
  217. package/dist/web/routes/sse.js +54 -0
  218. package/dist/web/routes/sse.js.map +1 -0
  219. package/package.json +33 -0
  220. package/package.json.license +2 -0
  221. package/src/auth/auth-service.ts +333 -0
  222. package/src/auth/http-server.ts +249 -0
  223. package/src/auth/jwt-secret.ts +65 -0
  224. package/src/auth/oidc/oidc-flow.ts +266 -0
  225. package/src/auth/oidc/oidc-provider-service.ts +174 -0
  226. package/src/auth/oidc/user-service.ts +227 -0
  227. package/src/auth/rate-limiter.ts +85 -0
  228. package/src/config/config-service.ts +434 -0
  229. package/src/config/validate.ts +153 -0
  230. package/src/content-types/registry.ts +194 -0
  231. package/src/datastore/db.ts +38 -0
  232. package/src/datastore/encrypt.ts +107 -0
  233. package/src/datastore/migrations.ts +367 -0
  234. package/src/datastore/storage.ts +83 -0
  235. package/src/events/event-bus.ts +180 -0
  236. package/src/events/spine.ts +87 -0
  237. package/src/events/trace.ts +38 -0
  238. package/src/host.ts +130 -0
  239. package/src/library/importer.ts +174 -0
  240. package/src/library/library-cache.ts +212 -0
  241. package/src/library/library-config.ts +22 -0
  242. package/src/library/library-scanner.ts +404 -0
  243. package/src/library/library-service.ts +282 -0
  244. package/src/library/metadata-matcher.ts +285 -0
  245. package/src/library/naming.ts +59 -0
  246. package/src/library/scan-controller.ts +490 -0
  247. package/src/library/source-resolver.ts +92 -0
  248. package/src/logging/logger.ts +115 -0
  249. package/src/main.ts +364 -0
  250. package/src/plugins/context.ts +138 -0
  251. package/src/plugins/discovery.ts +29 -0
  252. package/src/plugins/loader.ts +309 -0
  253. package/src/state/request-intake.ts +101 -0
  254. package/src/state/state-store.ts +621 -0
  255. package/src/state/trace-store.ts +27 -0
  256. package/src/types/plugin.ts +110 -0
  257. package/src/web/index.ts +182 -0
  258. package/src/web/routes/activity.ts +41 -0
  259. package/src/web/routes/auth.ts +170 -0
  260. package/src/web/routes/config.ts +282 -0
  261. package/src/web/routes/content-types.ts +42 -0
  262. package/src/web/routes/discovery.ts +156 -0
  263. package/src/web/routes/library-import.ts +196 -0
  264. package/src/web/routes/library-management.ts +140 -0
  265. package/src/web/routes/library.ts +46 -0
  266. package/src/web/routes/oidc.ts +260 -0
  267. package/src/web/routes/requests.ts +250 -0
  268. package/src/web/routes/sse.ts +92 -0
  269. package/tests/auth.test.ts +510 -0
  270. package/tests/config-route.test.ts +345 -0
  271. package/tests/config.test.ts +612 -0
  272. package/tests/content-types.test.ts +376 -0
  273. package/tests/datastore.test.ts +345 -0
  274. package/tests/e2e.test.ts +430 -0
  275. package/tests/event-bus.test.ts +925 -0
  276. package/tests/host.test.ts +449 -0
  277. package/tests/library-cache.test.ts +171 -0
  278. package/tests/library-scanner.test.ts +177 -0
  279. package/tests/library.test.ts +210 -0
  280. package/tests/loader-context.test.ts +644 -0
  281. package/tests/logger-jsonl.test.ts +154 -0
  282. package/tests/metadata-matcher.test.ts +198 -0
  283. package/tests/oidc.test.ts +218 -0
  284. package/tests/scan-controller.test.ts +328 -0
  285. package/tests/source-resolver.test.ts +100 -0
  286. package/tests/starr-video-integration.test.ts +153 -0
  287. package/tests/state-store.test.ts +700 -0
  288. package/tests/trace-history.test.ts +356 -0
  289. package/tsconfig.json +8 -0
  290. package/tsconfig.json.license +2 -0
@@ -0,0 +1,644 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ // SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
3
+
4
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
5
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import path from "node:path";
8
+ import Database from "better-sqlite3";
9
+ import { createLogger } from "../src/logging/logger.js";
10
+ import { createContext } from "../src/plugins/context.js";
11
+ import { loadPlugins, stopPlugins, reloadPlugin } from "../src/plugins/loader.js";
12
+ import { runMigrations, CORE_MIGRATIONS } from "../src/datastore/migrations.js";
13
+ import { Starr } from "../src/host.js";
14
+
15
+ // ── Helpers ──────────────────────────────────────────────────────────
16
+
17
+ function createTestDb(): Database.Database {
18
+ const db = new Database(":memory:");
19
+ db.pragma("journal_mode = WAL");
20
+ db.pragma("foreign_keys = ON");
21
+ runMigrations(db, CORE_MIGRATIONS, createLogger("test", "silent"));
22
+ return db;
23
+ }
24
+
25
+ async function writePlugin(
26
+ baseDir: string,
27
+ dirName: string,
28
+ code: string,
29
+ ): Promise<string> {
30
+ const dir = path.join(baseDir, dirName);
31
+ await mkdir(dir, { recursive: true });
32
+ await writeFile(
33
+ path.join(dir, "package.json"),
34
+ JSON.stringify({ type: "module", main: "./index.js" }),
35
+ );
36
+ await writeFile(path.join(dir, "index.js"), code);
37
+ return dir;
38
+ }
39
+
40
+ // ── Phase 1: PluginStorage.deleteAll ─────────────────────────────────
41
+
42
+ describe("PluginStorage.deleteAll", () => {
43
+ let db: Database.Database;
44
+
45
+ beforeAll(() => {
46
+ db = createTestDb();
47
+ });
48
+
49
+ afterAll(() => {
50
+ db.close();
51
+ });
52
+
53
+ it("removes all keys for a plugin", async () => {
54
+ const logger = createLogger("core", "silent");
55
+ const starr = new Starr({
56
+ pluginsDir: "/tmp",
57
+ apiVersion: "1.0.0",
58
+ logger,
59
+ });
60
+ const ctx = createContext(starr, "/tmp/test-plugin", db);
61
+ ctx.define({
62
+ id: "test-plugin",
63
+ version: "1.0.0",
64
+ starrApiVersion: "^1.0.0",
65
+ });
66
+
67
+ await ctx.storage.set("key1", "val1");
68
+ await ctx.storage.set("key2", "val2");
69
+
70
+ const before = await ctx.storage.list();
71
+ expect(before).toHaveLength(2);
72
+
73
+ await ctx.storage.deleteAll();
74
+
75
+ const after = await ctx.storage.list();
76
+ expect(after).toHaveLength(0);
77
+ });
78
+
79
+ it("does not affect another plugin's keys", async () => {
80
+ const logger = createLogger("core", "silent");
81
+ const starr = new Starr({
82
+ pluginsDir: "/tmp",
83
+ apiVersion: "1.0.0",
84
+ logger,
85
+ });
86
+
87
+ const ctxA = createContext(starr, "/tmp/a", db);
88
+ ctxA.define({ id: "a", version: "1.0.0", starrApiVersion: "^1.0.0" });
89
+ await ctxA.storage.set("key", "a-value");
90
+
91
+ const ctxB = createContext(starr, "/tmp/b", db);
92
+ ctxB.define({ id: "b", version: "1.0.0", starrApiVersion: "^1.0.0" });
93
+ await ctxB.storage.set("key", "b-value");
94
+
95
+ await ctxA.storage.deleteAll();
96
+
97
+ const aAfter = await ctxA.storage.list();
98
+ const bAfter = await ctxB.storage.list();
99
+
100
+ expect(aAfter).toHaveLength(0);
101
+ expect(bAfter).toHaveLength(1);
102
+ expect(bAfter[0].value).toBe("b-value");
103
+ });
104
+ });
105
+
106
+ // ── Phase 2: context unsubscribeAll ──────────────────────────────────
107
+
108
+ describe("context unsubscribeAll", () => {
109
+ let db: Database.Database;
110
+
111
+ beforeAll(() => {
112
+ db = createTestDb();
113
+ });
114
+
115
+ afterAll(() => {
116
+ db.close();
117
+ });
118
+
119
+ it("removes all event subscriptions", () => {
120
+ const logger = createLogger("core", "silent");
121
+ const starr = new Starr({
122
+ pluginsDir: "/tmp",
123
+ apiVersion: "1.0.0",
124
+ logger,
125
+ });
126
+ const ctx = createContext(starr, "/tmp/test-plugin", db);
127
+ ctx.define({
128
+ id: "test-plugin",
129
+ version: "1.0.0",
130
+ starrApiVersion: "^1.0.0",
131
+ });
132
+
133
+ const calls: string[] = [];
134
+ ctx.events.on("test.event", () => calls.push("a"));
135
+ ctx.events.on("test.event", () => calls.push("b"));
136
+
137
+ ctx.unsubscribeAll();
138
+
139
+ ctx.events.emit("test.event", {});
140
+ expect(calls).toHaveLength(0);
141
+ });
142
+
143
+ it("is safe to call when no subscriptions exist", () => {
144
+ const logger = createLogger("core", "silent");
145
+ const starr = new Starr({
146
+ pluginsDir: "/tmp",
147
+ apiVersion: "1.0.0",
148
+ logger,
149
+ });
150
+ const ctx = createContext(starr, "/tmp/test-plugin", db);
151
+ ctx.define({
152
+ id: "test-plugin",
153
+ version: "1.0.0",
154
+ starrApiVersion: "^1.0.0",
155
+ });
156
+
157
+ expect(() => ctx.unsubscribeAll()).not.toThrow();
158
+ });
159
+ });
160
+
161
+ // ── Phase 4: Rollback tests ──────────────────────────────────────────
162
+
163
+ describe("plugin register failure rollback", () => {
164
+ let tmpDir: string;
165
+
166
+ beforeAll(async () => {
167
+ tmpDir = await mkdtemp(path.join(tmpdir(), "starr-rollback-"));
168
+ });
169
+
170
+ afterAll(async () => {
171
+ await rm(tmpDir, { recursive: true, force: true });
172
+ });
173
+
174
+ it("rolls back event subscriptions when a plugin fails", async () => {
175
+ const subDir = await mkdtemp(path.join(tmpdir(), "starr-sub-rollback-"));
176
+ try {
177
+ // Plugin A subscribes to an event, then throws
178
+ await writePlugin(
179
+ subDir,
180
+ "failing-plugin",
181
+ `
182
+ export default function register(ctx) {
183
+ ctx.define({ id: "failing", version: "1.0.0", starrApiVersion: "^1.0.0" });
184
+ ctx.events.on("test.event", () => {});
185
+ throw new Error("boom after subscribe");
186
+ }
187
+ `,
188
+ );
189
+
190
+ await writePlugin(
191
+ subDir,
192
+ "observer-plugin",
193
+ `
194
+ export default function register(ctx) {
195
+ ctx.define({ id: "observer", version: "1.0.0", starrApiVersion: "^1.0.0" });
196
+ let count = 0;
197
+ ctx.events.on("test.event", () => { count++; });
198
+ return {
199
+ start() {
200
+ ctx.events.emit("test.event", {});
201
+ globalThis.__starr_observer_count = count;
202
+ }
203
+ };
204
+ }
205
+ `,
206
+ );
207
+
208
+ const db = createTestDb();
209
+ const logger = createLogger("core", "error");
210
+ const starr = new Starr({
211
+ pluginsDir: subDir,
212
+ apiVersion: "1.0.0",
213
+ logger,
214
+ });
215
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
216
+
217
+ await loadPlugins(starr);
218
+
219
+ // The failing plugin's subscription should have been rolled back.
220
+ // observer emits test.event in start — count should be 1 (only observer's
221
+ // own handler), not 2 (the failing plugin's handler should be gone).
222
+ expect((globalThis as any).__starr_observer_count).toBe(1);
223
+
224
+ delete (globalThis as any).__starr_observer_count;
225
+ db.close();
226
+ } finally {
227
+ await rm(subDir, { recursive: true, force: true });
228
+ }
229
+ });
230
+
231
+ it("rolls back content-type descriptors when a plugin fails", async () => {
232
+ const subDir = await mkdtemp(
233
+ path.join(tmpdir(), "starr-ct-rollback-"),
234
+ );
235
+ try {
236
+ await writePlugin(
237
+ subDir,
238
+ "ct-failing",
239
+ `
240
+ export default function register(ctx) {
241
+ ctx.define({ id: "ct-fail", version: "1.0.0", starrApiVersion: "^1.0.0" });
242
+ ctx.contentTypes.register({
243
+ id: "test/movie@1",
244
+ name: "Movie",
245
+ identity: ["tmdb"],
246
+ fields: [],
247
+ });
248
+ throw new Error("boom after content type");
249
+ }
250
+ `,
251
+ );
252
+
253
+ const db = createTestDb();
254
+ const logger = createLogger("core", "error");
255
+ const starr = new Starr({
256
+ pluginsDir: subDir,
257
+ apiVersion: "1.0.0",
258
+ logger,
259
+ });
260
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
261
+
262
+ await loadPlugins(starr);
263
+
264
+ // The content-type descriptor should have been rolled back
265
+ expect(
266
+ starr.contentTypeRegistry.get("test/movie@1"),
267
+ ).toBeUndefined();
268
+ db.close();
269
+ } finally {
270
+ await rm(subDir, { recursive: true, force: true });
271
+ }
272
+ });
273
+
274
+ it("rolls back plugin storage when a plugin fails", async () => {
275
+ const subDir = await mkdtemp(
276
+ path.join(tmpdir(), "starr-storage-rollback-"),
277
+ );
278
+ try {
279
+ await writePlugin(
280
+ subDir,
281
+ "storage-failing",
282
+ `
283
+ export default async function register(ctx) {
284
+ ctx.define({ id: "storage-fail", version: "1.0.0", starrApiVersion: "^1.0.0" });
285
+ await ctx.storage.set("key", "should-be-rolled-back");
286
+ throw new Error("boom after storage write");
287
+ }
288
+ `,
289
+ );
290
+
291
+ const db = createTestDb();
292
+ const logger = createLogger("core", "error");
293
+ const starr = new Starr({
294
+ pluginsDir: subDir,
295
+ apiVersion: "1.0.0",
296
+ logger,
297
+ });
298
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
299
+
300
+ await loadPlugins(starr);
301
+
302
+ // Storage should be cleaned up — create a new context to check
303
+ const checkCtx = createContext(
304
+ starr,
305
+ "/tmp/storage-fail",
306
+ db,
307
+ );
308
+ checkCtx.define({
309
+ id: "storage-fail",
310
+ version: "1.0.0",
311
+ starrApiVersion: "^1.0.0",
312
+ });
313
+ const val = await checkCtx.storage.get("key");
314
+ expect(val).toBeUndefined();
315
+ db.close();
316
+ } finally {
317
+ await rm(subDir, { recursive: true, force: true });
318
+ }
319
+ });
320
+ });
321
+
322
+ // ── Phase 3: Start/stop timeout tests ────────────────────────────────
323
+
324
+ describe("start/stop timeouts", () => {
325
+ it("start timeout does not block other plugins from starting", async () => {
326
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "starr-timeouts-start-"));
327
+ await writePlugin(
328
+ tmpDir,
329
+ "slow-starter",
330
+ `
331
+ export default function register(ctx) {
332
+ ctx.define({ id: "slow", version: "1.0.0", starrApiVersion: "^1.0.0" });
333
+ return {
334
+ start() {
335
+ return new Promise(() => {}); // never resolves
336
+ }
337
+ };
338
+ }
339
+ `,
340
+ );
341
+
342
+ await writePlugin(
343
+ tmpDir,
344
+ "fast-starter",
345
+ `
346
+ export default function register(ctx) {
347
+ ctx.define({ id: "fast", version: "1.0.0", starrApiVersion: "^1.0.0" });
348
+ return {
349
+ start() {
350
+ globalThis.__starr_fast_started = true;
351
+ }
352
+ };
353
+ }
354
+ `,
355
+ );
356
+
357
+ const db = createTestDb();
358
+ const logger = createLogger("core", "error");
359
+ const starr = new Starr({
360
+ pluginsDir: tmpDir,
361
+ apiVersion: "1.0.0",
362
+ logger,
363
+ });
364
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
365
+
366
+ // This should complete (not hang) — fast starter should start,
367
+ // slow starter should time out after 30s.
368
+ await loadPlugins(starr);
369
+
370
+ expect((globalThis as any).__starr_fast_started).toBe(true);
371
+
372
+ // slow plugin should NOT be in runtimes (timed out → startOrder = -1)
373
+ const runtimes = starr.getRuntimes();
374
+ const slowRuntime = runtimes.find((r) => r.meta.id === "slow");
375
+ expect(slowRuntime?.startOrder).toBe(-1);
376
+
377
+ await rm(tmpDir, { recursive: true, force: true });
378
+ delete (globalThis as any).__starr_fast_started;
379
+ db.close();
380
+ }, 40_000); // Extended timeout — the test waits for the 30s start timeout
381
+
382
+ it("stop timeout does not block shutdown", async () => {
383
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "starr-timeouts-stop-"));
384
+ await writePlugin(
385
+ tmpDir,
386
+ "slow-stopper",
387
+ `
388
+ export default function register(ctx) {
389
+ ctx.define({ id: "slow-stop", version: "1.0.0", starrApiVersion: "^1.0.0" });
390
+ return {
391
+ stop() {
392
+ return new Promise(() => {}); // never resolves
393
+ }
394
+ };
395
+ }
396
+ `,
397
+ );
398
+
399
+ await writePlugin(
400
+ tmpDir,
401
+ "fast-stopper",
402
+ `
403
+ export default function register(ctx) {
404
+ ctx.define({ id: "fast-stop", version: "1.0.0", starrApiVersion: "^1.0.0" });
405
+ return {
406
+ stop() {
407
+ globalThis.__starr_fast_stopped = true;
408
+ }
409
+ };
410
+ }
411
+ `,
412
+ );
413
+
414
+ const db = createTestDb();
415
+ const logger = createLogger("core", "error");
416
+ const starr = new Starr({
417
+ pluginsDir: tmpDir,
418
+ apiVersion: "1.0.0",
419
+ logger,
420
+ });
421
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
422
+
423
+ await loadPlugins(starr);
424
+ await stopPlugins(starr);
425
+
426
+ // Fast stopper should have run
427
+ expect((globalThis as any).__starr_fast_stopped).toBe(true);
428
+
429
+
430
+ await rm(tmpDir, { recursive: true, force: true });
431
+ delete (globalThis as any).__starr_fast_stopped;
432
+ db.close();
433
+ }, 20_000); // Extended timeout — the test waits for the 10s stop timeout
434
+ });
435
+
436
+ // ── Phase 5: reloadPlugin tests ──────────────────────────────────────
437
+
438
+ describe("reloadPlugin", () => {
439
+ it("stops, re-imports, and restarts a plugin", async () => {
440
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "starr-reload-seq-"));
441
+ const events: string[] = [];
442
+ const key = "__starr_reload_seq";
443
+ (globalThis as any)[key] = events;
444
+
445
+ await writePlugin(
446
+ tmpDir,
447
+ "reloadable",
448
+ `
449
+ const k = "${key}";
450
+ export default function register(ctx) {
451
+ globalThis[k].push("register");
452
+ ctx.define({ id: "reloadable", version: "1.0.0", starrApiVersion: "^1.0.0" });
453
+ return {
454
+ start() { globalThis[k].push("start"); },
455
+ stop() { globalThis[k].push("stop"); },
456
+ };
457
+ }
458
+ `,
459
+ );
460
+
461
+ const db = createTestDb();
462
+ const logger = createLogger("core", "error");
463
+ const starr = new Starr({
464
+ pluginsDir: tmpDir,
465
+ apiVersion: "1.0.0",
466
+ logger,
467
+ });
468
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
469
+
470
+ await loadPlugins(starr);
471
+
472
+ // Initial: register → start
473
+ expect(events).toEqual(["register", "start"]);
474
+
475
+ // Clear events for reload
476
+ events.length = 0;
477
+
478
+ await reloadPlugin(starr, "reloadable");
479
+
480
+ // Reload: stop → register → start
481
+ expect(events).toEqual(["stop", "register", "start"]);
482
+
483
+ await rm(tmpDir, { recursive: true, force: true });
484
+ delete (globalThis as any)[key];
485
+ db.close();
486
+ });
487
+
488
+ it("reload picks up code changes (cache bust)", async () => {
489
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "starr-reload-cache-"));
490
+ let value = 1;
491
+ const key = "__starr_reload_value";
492
+
493
+ const dir = path.join(tmpDir, "versioned-plugin");
494
+ await mkdir(dir, { recursive: true });
495
+ await writeFile(
496
+ path.join(dir, "package.json"),
497
+ JSON.stringify({ type: "module", main: "./index.js" }),
498
+ );
499
+ await writeFile(
500
+ path.join(dir, "index.js"),
501
+ `
502
+ export default function register(ctx) {
503
+ ctx.define({ id: "versioned", version: "1.0.0", starrApiVersion: "^1.0.0" });
504
+ return {
505
+ start() {
506
+ globalThis["${key}"] = ${value};
507
+ }
508
+ };
509
+ }
510
+ `,
511
+ );
512
+
513
+ const db = createTestDb();
514
+ const logger = createLogger("core", "error");
515
+ const starr = new Starr({
516
+ pluginsDir: tmpDir,
517
+ apiVersion: "1.0.0",
518
+ logger,
519
+ });
520
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
521
+
522
+ await loadPlugins(starr);
523
+ expect((globalThis as any)[key]).toBe(1);
524
+
525
+ // Update the source file
526
+ value = 2;
527
+ await writeFile(
528
+ path.join(dir, "index.js"),
529
+ `
530
+ export default function register(ctx) {
531
+ ctx.define({ id: "versioned", version: "1.0.0", starrApiVersion: "^1.0.0" });
532
+ return {
533
+ start() {
534
+ globalThis["${key}"] = ${value};
535
+ }
536
+ };
537
+ }
538
+ `,
539
+ );
540
+
541
+ await reloadPlugin(starr, "versioned");
542
+ expect((globalThis as any)[key]).toBe(2);
543
+
544
+ await rm(tmpDir, { recursive: true, force: true });
545
+ delete (globalThis as any)[key];
546
+ db.close();
547
+ });
548
+
549
+ it("reload of unknown plugin does not throw", async () => {
550
+ const tmpDir = await mkdtemp(path.join(tmpdir(), "starr-reload-unknown-"));
551
+ const db = createTestDb();
552
+ const logger = createLogger("core", "error");
553
+ const starr = new Starr({
554
+ pluginsDir: tmpDir,
555
+ apiVersion: "1.0.0",
556
+ logger,
557
+ });
558
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
559
+
560
+ await expect(
561
+ reloadPlugin(starr, "nonexistent"),
562
+ ).resolves.toBeUndefined();
563
+ await rm(tmpDir, { recursive: true, force: true });
564
+ db.close();
565
+ });
566
+ });
567
+
568
+ // ── Phase 4: Comprehensive rollback ──────────────────────────────────
569
+
570
+ describe("comprehensive rollback (L5)", () => {
571
+ let tmpDir: string;
572
+
573
+ beforeAll(async () => {
574
+ tmpDir = await mkdtemp(
575
+ path.join(tmpdir(), "starr-comprehensive-"),
576
+ );
577
+ });
578
+
579
+ afterAll(async () => {
580
+ await rm(tmpDir, { recursive: true, force: true });
581
+ });
582
+
583
+ it("rolls back subscriptions, descriptors, and storage on failure", async () => {
584
+ await writePlugin(
585
+ tmpDir,
586
+ "multi-fail",
587
+ `
588
+ export default async function register(ctx) {
589
+ ctx.define({ id: "multi-fail", version: "1.0.0", starrApiVersion: "^1.0.0" });
590
+ ctx.events.on("test.event", () => {});
591
+ ctx.contentTypes.register({
592
+ id: "fail/type@1",
593
+ name: "FailType",
594
+ identity: ["x"],
595
+ fields: [],
596
+ });
597
+ await ctx.storage.set("key", "value");
598
+ throw new Error("late failure");
599
+ }
600
+ `,
601
+ );
602
+
603
+ await writePlugin(
604
+ tmpDir,
605
+ "clean-plugin",
606
+ `
607
+ export default function register(ctx) {
608
+ ctx.define({ id: "clean", version: "1.0.0", starrApiVersion: "^1.0.0" });
609
+ }
610
+ `,
611
+ );
612
+
613
+ const db = createTestDb();
614
+ const logger = createLogger("core", "error");
615
+ const starr = new Starr({
616
+ pluginsDir: tmpDir,
617
+ apiVersion: "1.0.0",
618
+ logger,
619
+ });
620
+ starr.datastore = { db, dbPath: ":memory:", close: () => {} };
621
+
622
+ await loadPlugins(starr);
623
+
624
+ // multi-fail should be rejected
625
+ const runtimes = starr.getRuntimes();
626
+ expect(runtimes.map((r) => r.meta.id)).toEqual(["clean"]);
627
+
628
+ // Content-type descriptor should be rolled back
629
+ expect(
630
+ starr.contentTypeRegistry.get("fail/type@1"),
631
+ ).toBeUndefined();
632
+
633
+ // Storage should be cleaned up
634
+ const checkCtx = createContext(starr, "/tmp/multi-fail", db);
635
+ checkCtx.define({
636
+ id: "multi-fail",
637
+ version: "1.0.0",
638
+ starrApiVersion: "^1.0.0",
639
+ });
640
+ const val = await checkCtx.storage.get("key");
641
+ expect(val).toBeUndefined();
642
+ db.close();
643
+ });
644
+ });