"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const { XArtAudioEngine } = require("../src/lib/engine"); function createEngine() { return new XArtAudioEngine({ creditConfig: { baseCredits: 1, includedChars: 25000, stepChars: 10000, stepCredits: 1, maxCharsPerArticle: 120000, }, }); } test("returns not_article and does not charge caller", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-1"); const result = engine.processMention({ mentionPostId: "m1", callerUserId: "u1", parentPost: { id: "p1", text: "plain post" }, }); assert.equal(result.ok, false); assert.equal(result.status, "not_article"); assert.equal(engine.getWalletBalance("u1"), 5); }); test("charges credits and creates charged job for valid article", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-2"); const result = engine.processMention({ mentionPostId: "m2", callerUserId: "u1", parentPost: { id: "p2", authorId: "author-1", article: { id: "art-2", title: "Long Form", body: "hello ".repeat(2000), }, }, }); assert.equal(result.ok, true); assert.equal(result.job.status, "charged"); assert.equal(result.job.creditsCharged, 1); assert.equal(engine.getWalletBalance("u1"), 4); const ownerAccess = engine.checkAudioAccess(result.job.assetId, "u1"); assert.equal(ownerAccess.allowed, true); }); test("deduplicates repeated mention post ids", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-3"); const payload = { mentionPostId: "m3", callerUserId: "u1", parentPost: { id: "p3", authorId: "author-2", article: { id: "art-3", title: "Article", body: "text", }, }, }; const first = engine.processMention(payload); const second = engine.processMention(payload); assert.equal(first.ok, true); assert.equal(second.ok, true); assert.equal(second.deduped, true); assert.equal(engine.getWalletBalance("u1"), 4); }); test("non-owner must pay same credits to unlock, then has permanent access", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-4-owner"); engine.topUpCredits("u2", 5, "topup-4-viewer"); const result = engine.processMention({ mentionPostId: "m4", callerUserId: "u1", parentPost: { id: "p4", authorId: "author-2", article: { id: "art-4", title: "Article", body: "content", }, }, }); const checkBefore = engine.checkAudioAccess(result.job.assetId, "u2"); assert.equal(checkBefore.allowed, false); assert.equal(checkBefore.reason, "payment_required"); assert.equal(checkBefore.creditsRequired, result.job.creditsCharged); engine.unlockAudio(result.job.assetId, "u2"); assert.equal(engine.getWalletBalance("u2"), 4); const checkAfter = engine.checkAudioAccess(result.job.assetId, "u2"); assert.equal(checkAfter.allowed, true); engine.unlockAudio(result.job.assetId, "u2"); assert.equal(engine.getWalletBalance("u2"), 4); }); test("lists jobs for user newest first and provides summary", () => { const engine = createEngine(); engine.topUpCredits("u1", 10, "topup-5"); engine.processMention({ mentionPostId: "m5a", callerUserId: "u1", parentPost: { id: "p5a", authorId: "author-a", article: { id: "a5a", title: "One", body: "x" }, }, }); engine.processMention({ mentionPostId: "m5b", callerUserId: "u1", parentPost: { id: "p5b", authorId: "author-b", article: { id: "a5b", title: "Two", body: "y" }, }, }); const jobs = engine.listJobsForUser("u1"); assert.equal(jobs.length, 2); assert.equal(jobs[0].mentionPostId, "m5b"); assert.equal(jobs[1].mentionPostId, "m5a"); const summary = engine.getUserSummary("u1"); assert.equal(summary.totalJobs, 2); assert.equal(summary.completedJobs, 0); assert.equal(summary.totalCreditsSpent, 2); assert.equal(summary.balance, 8); }); test("job can transition through start and completion states", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-transition"); const created = engine.processMention({ mentionPostId: "m-transition", callerUserId: "u1", parentPost: { id: "p-transition", article: { id: "a-transition", title: "T", body: "hello world" }, }, }); const started = engine.startJob(created.job.id); assert.equal(started.status, "synthesizing"); const completed = engine.completeJob(created.job.id, { storageKey: "audio/final.mp3", sizeBytes: 42, }); assert.equal(completed.status, "completed"); assert.equal(engine.getAsset(created.job.assetId).storageKey, "audio/final.mp3"); assert.equal(engine.getAsset(created.job.assetId).sizeBytes, 42); }); test("failed generation can refund caller credits once", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-fail-refund"); const created = engine.processMention({ mentionPostId: "m-fail-refund", callerUserId: "u1", parentPost: { id: "p-fail-refund", article: { id: "a-fail-refund", title: "T", body: "hello world" }, }, }); assert.equal(engine.getWalletBalance("u1"), 4); engine.startJob(created.job.id); const failed = engine.failJob(created.job.id, { error: "tts_down", refund: true }); assert.equal(failed.status, "failed_refunded"); assert.equal(engine.getWalletBalance("u1"), 5); const second = engine.failJob(created.job.id, { error: "tts_down", refund: true }); assert.equal(second.status, "failed_refunded"); assert.equal(engine.getWalletBalance("u1"), 5); }); test("round-trips state snapshot across engine restart", () => { const engine1 = createEngine(); engine1.topUpCredits("u1", 5, "topup-snapshot"); const created = engine1.processMention({ mentionPostId: "m-snapshot", callerUserId: "u1", parentPost: { id: "p-snapshot", authorId: "author-snapshot", article: { id: "a-snapshot", title: "Snap", body: "hello world" }, }, }); const snapshot = engine1.exportState(); const engine2 = new XArtAudioEngine({ creditConfig: engine1.creditConfig, initialState: snapshot, }); assert.equal(engine2.getWalletBalance("u1"), 4); assert.equal(engine2.getJob(created.job.id).article.title, "Snap"); assert.equal(engine2.getAsset(created.job.assetId).articleTitle, "Snap"); assert.equal(engine2.checkAudioAccess(created.job.assetId, "u1").allowed, true); }); test("updateAsset patches stored asset metadata", () => { const engine = createEngine(); engine.topUpCredits("u1", 5, "topup-update-asset"); const created = engine.processMention({ mentionPostId: "m-update-asset", callerUserId: "u1", parentPost: { id: "p-update-asset", authorId: "author", article: { id: "a-update", title: "T", body: "hello" }, }, }); const updated = engine.updateAsset(created.job.assetId, { storageKey: "audio/real-file.mp3", sizeBytes: 12345, }); assert.equal(updated.storageKey, "audio/real-file.mp3"); assert.equal(updated.sizeBytes, 12345); }); test("owner can takedown audio and hide it from access checks", () => { const engine = createEngine(); engine.topUpCredits("owner", 5, "topup-takedown"); const created = engine.processMention({ mentionPostId: "m-takedown", callerUserId: "owner", parentPost: { id: "p-takedown", article: { id: "a-takedown", title: "T", body: "hello" }, }, }); engine.takedownAudio(created.job.assetId, "owner"); assert.equal(engine.getAsset(created.job.assetId), null); assert.equal(engine.getAsset(created.job.assetId, { includeDeleted: true }).deletedAt !== null, true); assert.equal(engine.checkAudioAccess(created.job.assetId, "owner").reason, "not_found"); }); test("retention prunes old article content and deletes stale assets", () => { const engine = createEngine(); engine.topUpCredits("owner", 5, "topup-retention"); const created = engine.processMention({ mentionPostId: "m-retention", callerUserId: "owner", parentPost: { id: "p-retention", article: { id: "a-retention", title: "T", body: "hello retention" }, }, }); const job = engine.getJob(created.job.id); const asset = engine.getAsset(created.job.assetId, { includeDeleted: true }); job.createdAt = "2020-01-01T00:00:00.000Z"; asset.createdAt = "2020-01-01T00:00:00.000Z"; const summary = engine.applyRetention({ rawArticleHours: 1, audioDays: 1, now: new Date("2020-01-03T00:00:00.000Z"), }); assert.equal(summary.prunedArticleBodies, 1); assert.equal(summary.deletedAssets, 1); assert.equal(engine.getJob(created.job.id).article.content, ""); assert.equal(typeof engine.getJob(created.job.id).article.contentHash, "string"); assert.equal(engine.getAsset(created.job.assetId), null); });