Laimiao agent v2 service: agent orchestration, sessions, billing, gateway client, skills catalog, auth, config, plus tests.
90 lines
3.4 KiB
TypeScript
90 lines
3.4 KiB
TypeScript
import { appendFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { UsageLedger, type BeginUsage } from "../src/billing/ledger.js";
|
|
import { silentLogger, tempRoot } from "./helpers.js";
|
|
|
|
function usage(overrides: Partial<BeginUsage> = {}): BeginUsage {
|
|
return {
|
|
uid: "1",
|
|
team_id: "1",
|
|
session_id: "s1",
|
|
tool_call_id: "call-1",
|
|
skill_id: "demo",
|
|
skill_name: "Demo",
|
|
model: "seedream@5.0",
|
|
size: "1024x1536",
|
|
quantity: 2,
|
|
unit_credits: 30,
|
|
size_multiplier: 1,
|
|
total_credits: 60,
|
|
currency: "credits",
|
|
billing_mode: "dry_run",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("UsageLedger", () => {
|
|
it("records pending and succeeded usage and finds it by tool call", () => {
|
|
const ledger = new UsageLedger(join(tempRoot(), "ledger.jsonl"), silentLogger);
|
|
const record = ledger.begin(usage());
|
|
expect(record.status).toBe("pending");
|
|
|
|
ledger.settle(record.id, "succeeded", {
|
|
gateway_task_id: "task-1",
|
|
image_urls: ["https://img/1.png", "https://img/2.png"],
|
|
});
|
|
|
|
const found = ledger.findSucceededToolCall("1", "s1", "call-1");
|
|
expect(found?.id).toBe(record.id);
|
|
expect(found?.status).toBe("succeeded");
|
|
expect(found?.image_urls).toHaveLength(2);
|
|
expect(ledger.findSucceededToolCall("2", "s1", "call-1")).toBeUndefined();
|
|
});
|
|
|
|
it("persists records and rebuilds the idempotency index", () => {
|
|
const file = join(tempRoot(), "ledger.jsonl");
|
|
const ledger = new UsageLedger(file, silentLogger);
|
|
const record = ledger.begin(usage());
|
|
ledger.settle(record.id, "succeeded", { image_urls: ["https://img/1.png"] });
|
|
|
|
const reloaded = new UsageLedger(file, silentLogger);
|
|
expect(reloaded.list()).toHaveLength(1);
|
|
expect(reloaded.findSucceededToolCall("1", "s1", "call-1")?.id).toBe(record.id);
|
|
});
|
|
|
|
it("does not index failed calls and summarises by status and skill", () => {
|
|
const ledger = new UsageLedger(join(tempRoot(), "ledger.jsonl"), silentLogger);
|
|
const failed = ledger.begin(usage({ tool_call_id: "call-failed" }));
|
|
ledger.settle(failed.id, "failed", { error: "nope" });
|
|
const ok = ledger.begin(usage({ tool_call_id: "call-ok", skill_id: "poster", total_credits: 30, quantity: 1 }));
|
|
ledger.settle(ok.id, "succeeded", { image_urls: ["https://img/p.png"] });
|
|
const pending = ledger.begin(usage({ tool_call_id: "call-pending", skill_id: "poster", total_credits: 30 }));
|
|
|
|
expect(ledger.findSucceededToolCall("1", "s1", "call-failed")).toBeUndefined();
|
|
const summary = ledger.summary({ uid: "1" });
|
|
expect(summary.totals).toEqual({
|
|
records: 3,
|
|
images: 1,
|
|
credits_succeeded: 30,
|
|
credits_pending: 30,
|
|
credits_failed: 60,
|
|
});
|
|
expect(summary.by_skill).toEqual([
|
|
{ skill_id: "poster", skill_name: "Demo", records: 2, images: 1, credits: 30 },
|
|
{ skill_id: "demo", skill_name: "Demo", records: 1, images: 0, credits: 0 },
|
|
]);
|
|
expect(ledger.list({ status: "pending" }).map((item) => item.id)).toEqual([pending.id]);
|
|
expect(ledger.list({ limit: 1 })).toHaveLength(1);
|
|
});
|
|
|
|
it("skips malformed lines instead of losing the ledger", () => {
|
|
const file = join(tempRoot(), "ledger.jsonl");
|
|
const ledger = new UsageLedger(file, silentLogger);
|
|
ledger.begin(usage());
|
|
appendFileSync(file, "{bad json\n", "utf8");
|
|
const reloaded = new UsageLedger(file, silentLogger);
|
|
expect(reloaded.list()).toHaveLength(1);
|
|
});
|
|
});
|