@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.
- package/README.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { EventBusImpl } from "../src/events/event-bus.js";
|
|
7
|
+
import { createLogger } from "../src/logging/logger.js";
|
|
8
|
+
import { runMigrations, CORE_MIGRATIONS } from "../src/datastore/migrations.js";
|
|
9
|
+
import { StateStore } from "../src/state/state-store.js";
|
|
10
|
+
import type { EventBus } from "../src/types/plugin.js";
|
|
11
|
+
|
|
12
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function setup() {
|
|
15
|
+
const db = new Database(":memory:");
|
|
16
|
+
db.pragma("journal_mode = WAL");
|
|
17
|
+
db.pragma("foreign_keys = ON");
|
|
18
|
+
const logger = createLogger("test", "silent");
|
|
19
|
+
runMigrations(db, CORE_MIGRATIONS, logger);
|
|
20
|
+
|
|
21
|
+
const impl = new EventBusImpl(logger);
|
|
22
|
+
const coreBus: EventBus = impl.coreBus();
|
|
23
|
+
|
|
24
|
+
const store = new StateStore(db, coreBus, logger);
|
|
25
|
+
|
|
26
|
+
return { db, impl, coreBus, store };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Insert a tracked request and move it to "searching" so handlers accept events. */
|
|
30
|
+
function insertRequest(store: StateStore, requestId: string) {
|
|
31
|
+
store.insert({
|
|
32
|
+
requestId,
|
|
33
|
+
contentTypeId: "starr-video/movie@1",
|
|
34
|
+
contentIdentity: "starr-video/movie@1::tmdb=" + requestId,
|
|
35
|
+
item: {
|
|
36
|
+
contentTypeId: "starr-video/movie@1",
|
|
37
|
+
title: "Test Movie",
|
|
38
|
+
externalIds: { tmdb: requestId },
|
|
39
|
+
},
|
|
40
|
+
ownerUserId: "admin",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Trace capture ────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe("trace capture", () => {
|
|
47
|
+
it("captures release.found events", () => {
|
|
48
|
+
const { coreBus, store } = setup();
|
|
49
|
+
const reqId = "req-capture-1";
|
|
50
|
+
insertRequest(store, reqId);
|
|
51
|
+
|
|
52
|
+
coreBus.emit("release.found", {
|
|
53
|
+
requestId: reqId,
|
|
54
|
+
contentTypeId: "starr-video/movie@1",
|
|
55
|
+
release: { title: "Dune 1080p", seeders: 42 },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const trace = store.getTraceLog(reqId);
|
|
59
|
+
expect(trace.length).toBe(1);
|
|
60
|
+
expect(trace[0].eventType).toBe("release.found");
|
|
61
|
+
expect(trace[0].requestId).toBe(reqId);
|
|
62
|
+
expect(trace[0].traceId).toBeTruthy(); // host-assigned
|
|
63
|
+
expect(trace[0].eventData.payload.release.title).toBe("Dune 1080p");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("captures download.started, download.completed, download.failed, and request.failed events", () => {
|
|
67
|
+
const { coreBus, store } = setup();
|
|
68
|
+
const reqId = "req-capture-2";
|
|
69
|
+
insertRequest(store, reqId);
|
|
70
|
+
|
|
71
|
+
coreBus.emit("release.found", {
|
|
72
|
+
requestId: reqId,
|
|
73
|
+
contentTypeId: "starr-video/movie@1",
|
|
74
|
+
release: {},
|
|
75
|
+
});
|
|
76
|
+
coreBus.emit("download.started", {
|
|
77
|
+
requestId: reqId,
|
|
78
|
+
release: { id: "rel-1" },
|
|
79
|
+
});
|
|
80
|
+
coreBus.emit("download.completed", {
|
|
81
|
+
requestId: reqId,
|
|
82
|
+
release: {},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const trace = store.getTraceLog(reqId);
|
|
86
|
+
const types = trace.map((t) => t.eventType);
|
|
87
|
+
expect(types).toContain("release.found");
|
|
88
|
+
expect(types).toContain("download.started");
|
|
89
|
+
expect(types).toContain("download.completed");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("captures transient download.failed events (retry path)", () => {
|
|
93
|
+
const { coreBus, store } = setup();
|
|
94
|
+
const reqId = "req-capture-3";
|
|
95
|
+
insertRequest(store, reqId);
|
|
96
|
+
|
|
97
|
+
coreBus.emit("release.found", {
|
|
98
|
+
requestId: reqId,
|
|
99
|
+
contentTypeId: "starr-video/movie@1",
|
|
100
|
+
release: {},
|
|
101
|
+
});
|
|
102
|
+
coreBus.emit("download.started", {
|
|
103
|
+
requestId: reqId,
|
|
104
|
+
release: { id: "rel-1" },
|
|
105
|
+
});
|
|
106
|
+
coreBus.emit("download.failed", {
|
|
107
|
+
requestId: reqId,
|
|
108
|
+
release: {},
|
|
109
|
+
error: "connection refused",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const trace = store.getTraceLog(reqId);
|
|
113
|
+
const failEntry = trace.find((t) => t.eventType === "download.failed");
|
|
114
|
+
expect(failEntry).toBeDefined();
|
|
115
|
+
expect(failEntry!.eventData.payload.error).toBe("connection refused");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("captures request.failed events", () => {
|
|
119
|
+
const { coreBus, store } = setup();
|
|
120
|
+
const reqId = "req-capture-4";
|
|
121
|
+
insertRequest(store, reqId);
|
|
122
|
+
|
|
123
|
+
coreBus.emit("release.found", {
|
|
124
|
+
requestId: reqId,
|
|
125
|
+
contentTypeId: "starr-video/movie@1",
|
|
126
|
+
release: {},
|
|
127
|
+
});
|
|
128
|
+
coreBus.emit("request.failed", {
|
|
129
|
+
requestId: reqId,
|
|
130
|
+
reason: "no acceptable candidates",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const trace = store.getTraceLog(reqId);
|
|
134
|
+
const failEntry = trace.find((t) => t.eventType === "request.failed");
|
|
135
|
+
expect(failEntry).toBeDefined();
|
|
136
|
+
expect(failEntry!.eventData.payload.reason).toBe("no acceptable candidates");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("stores envelope fields (traceId, correlationId, source, timestamp)", () => {
|
|
140
|
+
const { coreBus, store } = setup();
|
|
141
|
+
const reqId = "req-capture-5";
|
|
142
|
+
insertRequest(store, reqId);
|
|
143
|
+
|
|
144
|
+
coreBus.emit("release.found", {
|
|
145
|
+
requestId: reqId,
|
|
146
|
+
contentTypeId: "starr-video/movie@1",
|
|
147
|
+
release: { title: "Test" },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const trace = store.getTraceLog(reqId);
|
|
151
|
+
expect(trace.length).toBe(1);
|
|
152
|
+
|
|
153
|
+
const data = trace[0].eventData;
|
|
154
|
+
expect(data.traceId).toBeTruthy();
|
|
155
|
+
expect(typeof data.traceId).toBe("string");
|
|
156
|
+
expect(data.source).toBeTruthy();
|
|
157
|
+
expect(typeof data.source).toBe("string");
|
|
158
|
+
expect(data.timestamp).toBeTruthy();
|
|
159
|
+
// timestamp is ISO 8601 from the event envelope
|
|
160
|
+
expect(data.timestamp as string).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
161
|
+
expect(typeof data.eventId).toBe("string");
|
|
162
|
+
expect(typeof data.schemaVersion).toBe("number");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── Defensive: unknown requestId ─────────────────────────────────
|
|
167
|
+
|
|
168
|
+
describe("defensive: unknown requestId", () => {
|
|
169
|
+
it("does NOT capture events for unknown requestIds", () => {
|
|
170
|
+
const { coreBus, store } = setup();
|
|
171
|
+
|
|
172
|
+
// Emit an event with a requestId that has no tracked request
|
|
173
|
+
coreBus.emit("release.found", {
|
|
174
|
+
requestId: "nonexistent",
|
|
175
|
+
contentTypeId: "starr-video/movie@1",
|
|
176
|
+
release: {},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const trace = store.getTraceLog("nonexistent");
|
|
180
|
+
expect(trace.length).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── Trace ordering ───────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe("trace ordering", () => {
|
|
187
|
+
it("returns events in insertion order (oldest first)", () => {
|
|
188
|
+
const { coreBus, store } = setup();
|
|
189
|
+
const reqId = "req-order";
|
|
190
|
+
insertRequest(store, reqId);
|
|
191
|
+
|
|
192
|
+
// Emit events in a specific order
|
|
193
|
+
coreBus.emit("release.found", {
|
|
194
|
+
requestId: reqId,
|
|
195
|
+
contentTypeId: "starr-video/movie@1",
|
|
196
|
+
release: { title: "First" },
|
|
197
|
+
});
|
|
198
|
+
coreBus.emit("release.found", {
|
|
199
|
+
requestId: reqId,
|
|
200
|
+
contentTypeId: "starr-video/movie@1",
|
|
201
|
+
release: { title: "Second" },
|
|
202
|
+
});
|
|
203
|
+
coreBus.emit("release.found", {
|
|
204
|
+
requestId: reqId,
|
|
205
|
+
contentTypeId: "starr-video/movie@1",
|
|
206
|
+
release: { title: "Third" },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const trace = store.getTraceLog(reqId);
|
|
210
|
+
expect(trace.length).toBe(3);
|
|
211
|
+
expect(trace[0].eventData.payload.release.title).toBe("First");
|
|
212
|
+
expect(trace[1].eventData.payload.release.title).toBe("Second");
|
|
213
|
+
expect(trace[2].eventData.payload.release.title).toBe("Third");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── Pruning ──────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe("pruning (bounded trace)", () => {
|
|
220
|
+
it("prunes oldest entries when traceId exceeds the limit", () => {
|
|
221
|
+
const { coreBus, store } = setup();
|
|
222
|
+
const reqId = "req-prune";
|
|
223
|
+
insertRequest(store, reqId);
|
|
224
|
+
|
|
225
|
+
// The default limit is 50. Emit 55 events for the same requestId
|
|
226
|
+
// (which also share the same traceId since they're all emitted
|
|
227
|
+
// outside a handler context — each emit mints a new traceId...
|
|
228
|
+
// Wait — they are emitted from the test, not from within a handler,
|
|
229
|
+
// so each emit starts a new chain with a unique traceId.
|
|
230
|
+
//
|
|
231
|
+
// To test pruning, we need events that share a traceId. We can
|
|
232
|
+
// achieve this by emitting from within a handler so the traceId
|
|
233
|
+
// is inherited (PS2).
|
|
234
|
+
|
|
235
|
+
// Subscribe to a custom event and emit all 55 inside its handler
|
|
236
|
+
// so they share the same traceId.
|
|
237
|
+
let capturedTraceId: string | null = null;
|
|
238
|
+
|
|
239
|
+
coreBus.on("test.batch", (event) => {
|
|
240
|
+
capturedTraceId = event.traceId;
|
|
241
|
+
// Emit 55 spine events inside this handler — they all inherit
|
|
242
|
+
// the handler's traceId (PS2).
|
|
243
|
+
for (let i = 0; i < 55; i++) {
|
|
244
|
+
coreBus.emit("release.found", {
|
|
245
|
+
requestId: reqId,
|
|
246
|
+
contentTypeId: "starr-video/movie@1",
|
|
247
|
+
release: { index: i },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Emit the trigger event to start the chain
|
|
253
|
+
coreBus.emit("test.batch", {});
|
|
254
|
+
|
|
255
|
+
expect(capturedTraceId).toBeTruthy();
|
|
256
|
+
|
|
257
|
+
const trace = store.getTraceLog(reqId);
|
|
258
|
+
// Should have at most 50 entries (pruned 5)
|
|
259
|
+
expect(trace.length).toBeLessThanOrEqual(50);
|
|
260
|
+
|
|
261
|
+
// The OLDEST 5 entries (index 0-4) should be gone.
|
|
262
|
+
// The REMAINING entries should be the most recent 50 (indexes 5-54).
|
|
263
|
+
if (trace.length === 50) {
|
|
264
|
+
const indices = trace.map(
|
|
265
|
+
(t) => (t.eventData.payload as { release: { index: number } }).release
|
|
266
|
+
.index,
|
|
267
|
+
);
|
|
268
|
+
// The lowest index should be 5 (entries 0-4 were pruned)
|
|
269
|
+
expect(Math.min(...indices)).toBe(5);
|
|
270
|
+
// The highest index should be 54
|
|
271
|
+
expect(Math.max(...indices)).toBe(54);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── Trace disabling (TRACE_HISTORY_LIMIT=0) ──────────────────────
|
|
277
|
+
|
|
278
|
+
describe("trace capture disabled", () => {
|
|
279
|
+
it("default limit (50) captures events normally", () => {
|
|
280
|
+
const { coreBus, store } = setup();
|
|
281
|
+
const reqId = "req-default-limit";
|
|
282
|
+
insertRequest(store, reqId);
|
|
283
|
+
|
|
284
|
+
coreBus.emit("release.found", {
|
|
285
|
+
requestId: reqId,
|
|
286
|
+
contentTypeId: "starr-video/movie@1",
|
|
287
|
+
release: {},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const trace = store.getTraceLog(reqId);
|
|
291
|
+
expect(trace.length).toBe(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("manual: STARR_TRACE_HISTORY_LIMIT=0 disables capture", () => {
|
|
295
|
+
// This test cannot run in-process because TRACE_HISTORY_LIMIT is
|
|
296
|
+
// computed at module-load time. See the Verification section for
|
|
297
|
+
// the manual test procedure.
|
|
298
|
+
expect(true).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ── Integration: trace survives across the full lifecycle ────────
|
|
303
|
+
|
|
304
|
+
describe("integration: full lifecycle trace", () => {
|
|
305
|
+
it("captures the complete happy-path trace", () => {
|
|
306
|
+
const { coreBus, store } = setup();
|
|
307
|
+
const reqId = "req-lifecycle";
|
|
308
|
+
insertRequest(store, reqId);
|
|
309
|
+
|
|
310
|
+
// Full happy path
|
|
311
|
+
coreBus.emit("release.found", {
|
|
312
|
+
requestId: reqId,
|
|
313
|
+
contentTypeId: "starr-video/movie@1",
|
|
314
|
+
release: { title: "Candidate 1" },
|
|
315
|
+
});
|
|
316
|
+
coreBus.emit("release.found", {
|
|
317
|
+
requestId: reqId,
|
|
318
|
+
contentTypeId: "starr-video/movie@1",
|
|
319
|
+
release: { title: "Candidate 2" },
|
|
320
|
+
});
|
|
321
|
+
coreBus.emit("download.started", {
|
|
322
|
+
requestId: reqId,
|
|
323
|
+
release: { id: "rel-1", title: "Dune 1080p" },
|
|
324
|
+
});
|
|
325
|
+
coreBus.emit("download.completed", {
|
|
326
|
+
requestId: reqId,
|
|
327
|
+
release: { id: "rel-1" },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const trace = store.getTraceLog(reqId);
|
|
331
|
+
expect(trace.length).toBe(4);
|
|
332
|
+
|
|
333
|
+
const types = trace.map((t) => t.eventType);
|
|
334
|
+
expect(types).toEqual([
|
|
335
|
+
"release.found",
|
|
336
|
+
"release.found",
|
|
337
|
+
"download.started",
|
|
338
|
+
"download.completed",
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
// Verify the data is preserved
|
|
342
|
+
const startedEvent = trace.find(
|
|
343
|
+
(t) => t.eventType === "download.started",
|
|
344
|
+
);
|
|
345
|
+
expect(startedEvent).toBeDefined();
|
|
346
|
+
expect(
|
|
347
|
+
(startedEvent!.eventData.payload as { release: { title: string } })
|
|
348
|
+
.release.title,
|
|
349
|
+
).toBe("Dune 1080p");
|
|
350
|
+
|
|
351
|
+
// Verify timestamps are in order (each is ISO 8601)
|
|
352
|
+
for (let i = 1; i < trace.length; i++) {
|
|
353
|
+
expect(trace[i].timestamp >= trace[i - 1].timestamp).toBe(true);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
package/tsconfig.json
ADDED