Files
xarticleaudio/test/engine.test.js

302 lines
8.8 KiB
JavaScript

"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);
});