const assert = require("assert"); const fs = require("fs"); const os = require("os"); const path = require("path"); const Database = require("better-sqlite3"); const isolatedDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-user-age-")); process.env.LUMI_DATA_DIR = isolatedDataDir; const { formatDuration, parseDurationFormat } = require("../src/services/duration-format"); const { UserAgeStatistics } = require("../src/services/user-age-statistics"); const placeholders = require("../src/services/placeholders"); const coreDatabase = require("../src/services/db"); coreDatabase.migrate(); const hour = 60 * 60 * 1000; const day = 24 * hour; let now = 100 * day; const database = new Database(":memory:"); database.exec(` CREATE TABLE user_profiles ( id TEXT PRIMARY KEY, internal_username TEXT NOT NULL ); CREATE TABLE user_identities ( user_id TEXT NOT NULL, provider TEXT NOT NULL, provider_user_id TEXT NOT NULL ); `); const service = new UserAgeStatistics(database, { now: () => now }); database.prepare("INSERT INTO user_profiles (id, internal_username) VALUES (?, ?)").run("profile-1", "ViewerOne"); database.prepare("INSERT INTO user_identities (user_id, provider, provider_user_id) VALUES (?, ?, ?)").run("profile-1", "twitch", "viewer-1"); const base = { platform: "twitch", userId: "viewer-1", scopeId: "channel-1", type: "follow", source: "test-event", authoritative: true }; service.setState({ ...base, active: true, startAt: now - 10 * day, occurredAt: now, eventId: "follow-1" }); let rows = service.listDiagnostics({ platform: "twitch", userId: "viewer-1", type: "follow" }); assert.strictEqual(rows.length, 1, "new follow should create one interval"); assert.strictEqual(rows[0].end_at, null, "new follow interval should remain open"); now += 2 * day; service.setState({ ...base, active: false, occurredAt: now, eventId: "unfollow-1" }); rows = service.listDiagnostics({ platform: "twitch", userId: "viewer-1", type: "follow" }); assert.strictEqual(rows[0].end_at, now, "unfollow should close the open interval"); const firstCompleted = { ...rows[0] }; now += day; service.setState({ ...base, active: true, startAt: now, occurredAt: now, eventId: "follow-2" }); service.setState({ ...base, active: true, startAt: now, occurredAt: now, eventId: "follow-2" }); rows = service.listDiagnostics({ platform: "twitch", userId: "viewer-1", type: "follow" }); assert.strictEqual(rows.length, 2, "refollow should create exactly one second interval"); assert.deepStrictEqual( Object.fromEntries(Object.entries(rows.find((row) => row.id === firstCompleted.id)).filter(([key]) => !["updated_at"].includes(key))), Object.fromEntries(Object.entries(firstCompleted).filter(([key]) => !["updated_at"].includes(key))), "completed intervals must not be modified" ); now += 3 * day; let durations = service.getDurations(base); assert.strictEqual(durations.current, 3 * day, "current should include only the open interval"); assert.strictEqual(durations.total, 15 * day, "total should combine completed and active intervals"); const leaders = service.getLeaderboard("follow", { platform: "twitch", limit: 10 }); assert.strictEqual(leaders[0].username, "ViewerOne", "leaderboards should resolve linked Lumi profiles"); assert.ok(leaders.some((entry) => entry.label.includes("channel-1")), "leaderboards should keep channel scopes visible and separate"); service.setState({ ...base, active: false, occurredAt: now, eventId: "unfollow-2" }); durations = service.getDurations(base); assert.strictEqual(durations.current, null, "inactive state should have no current duration"); service.setState({ ...base, scopeId: "channel-2", active: true, occurredAt: now, eventId: "channel-2-follow" }); assert.strictEqual(service.getDurations({ ...base, scopeId: "channel-1" }).current, null, "channels must remain isolated"); assert.strictEqual(service.getDurations({ ...base, scopeId: "channel-2" }).current, 0, "second channel should have its own interval"); service.setState({ platform: "discord", userId: "viewer-1", scopeId: "guild-1", type: "member", active: true, occurredAt: now, eventId: "guild-1-member", source: "test" }); assert.strictEqual(service.getDurations({ platform: "discord", userId: "viewer-1", scopeId: "guild-2", type: "member" }).total, null, "Discord guilds must remain isolated"); const combined = 2 * 30 * day + 3 * day + 4 * hour + 47 * 60 * 1000; assert.strictEqual(formatDuration(combined, "MM-DD-mm"), "2 months, 3 days, 287 minutes", "omitted hours should flow into minutes"); assert.strictEqual(formatDuration(3 * day + 2 * hour, "HH-mm"), "74 hours", "larger omitted units should flow into the largest selected unit"); assert.strictEqual(formatDuration(287 * 60 * 1000, "mm"), "287 minutes", "minutes-only format should contain total minutes"); assert.strictEqual(formatDuration(day + hour + 60 * 1000, "DD-HH-mm"), "1 day, 1 hour, 1 minute", "singular labels should be correct"); assert.strictEqual(formatDuration(2 * day + 2 * hour + 2 * 60 * 1000, "D-H-m"), "2 days, 2 hours, 2 minutes", "plural labels should be correct"); assert.strictEqual(formatDuration(500, "s"), "", "all-zero selected units should return an empty string"); assert.throws(() => parseDurationFormat("DD-YY"), /largest to smallest/, "out-of-order units should fail deterministically"); const restarted = new UserAgeStatistics(database, { now: () => now }); const beforeRestart = restarted.listDiagnostics({ platform: "twitch", scopeId: "channel-2", type: "follow" }); restarted.reconcileSnapshot({ platform: "twitch", scopeId: "channel-2", type: "follow", activeRecords: [{ userId: "viewer-1" }], observedAt: now, source: "restart-test", complete: true }); const afterRestart = restarted.listDiagnostics({ platform: "twitch", scopeId: "channel-2", type: "follow" }); assert.strictEqual(afterRestart.length, beforeRestart.length, "restart reconciliation must not duplicate valid intervals"); assert.strictEqual(afterRestart[0].end_at, null, "restart reconciliation must not close valid intervals"); restarted.setState({ platform: "youtube", userId: "viewer-2", scopeId: "youtube-channel", type: "subscriber", active: true, occurredAt: now, eventId: "youtube-observed", source: "youtube-first-observed", authoritative: false }); const observed = restarted.listDiagnostics({ platform: "youtube", userId: "viewer-2" })[0]; assert.strictEqual(observed.start_at, now, "unavailable historical starts must begin at first observation"); assert.strictEqual(observed.start_authoritative, 0, "first-observed timestamps must not be marked authoritative"); placeholders.registerCorePlaceholders({ userAgeStatistics: restarted }); const invalidFormat = placeholders.validateTemplate({ fieldId: "core.custom_commands.static_response", template: "{{twitch.user.follow_age.current}.{format.YYY-DD}}", outputAudience: "user", runtimeContext: { runtime: true } }); assert.strictEqual(invalidFormat.ok, false, "unsupported duration tokens should fail shared placeholder validation"); const existing = placeholders.renderTemplate({ fieldId: "core.custom_commands.static_response", template: "Hello {{user.public.display_name}}", outputAudience: "user", runtimeContext: { runtime: true, user: { displayName: "Lumi Friend" } } }); const formatted = placeholders.renderTemplate({ fieldId: "core.custom_commands.static_response", template: "{{twitch.user.follow_age.current}.{format.DD-HH-mm}}", outputAudience: "user", runtimeContext: { runtime: true, platform: "twitch", user: { platformId: "viewer-1" }, ctx: { meta: { tags: { "room-id": "channel-2" } } } } }); Promise.all([existing, formatted]).then(([existingResult, formattedResult]) => { assert.strictEqual(existingResult.rendered, "Hello Lumi Friend", "existing placeholders must remain unchanged"); assert.strictEqual(formattedResult.rendered, "", "a zero-duration current interval should render empty"); database.close(); coreDatabase.db.close(); fs.rmSync(isolatedDataDir, { recursive: true, force: true }); console.log("User-age statistics verification passed."); }).catch((error) => { database.close(); coreDatabase.db.close(); fs.rmSync(isolatedDataDir, { recursive: true, force: true }); console.error(error); process.exit(1); });