Files
AgentHub/server/__tests__/pricing-sync.test.js
T
nntrivi2001 158783d5d5 feat(lanes,run,pricing): Changes sidebar editor, staged groups, image drop, auto pricing
- Changes panel at full screen: the whole Files view (finder, expand-all,
  tree) becomes a left sidebar and the Files tab is dropped; a working-copy
  file opens straight into the editor (Save/Cancel live once the text
  differs, Cancel restores it, Whole file toggles back to the diff), and the
  page is height-locked so the editor no longer collapses with no live run.
- Overview splits into Staged Changes / Changes groups in the work range.
- A changed file's whole tree row takes its status tint.
- Drag an image onto the lane terminal: POST /api/run/drop-image saves it
  (png/jpeg/gif/webp, random name, 0600, pruned after a day) and the path
  is pasted into the pane, which is how claude attaches it.
- Claude Opus 5.5 gets its own pricing row (4/20/0.2/5/8, fast 8/40);
  claude-opus-5% had been pricing it as Opus 5.
- server/lib/pricing-sync.js: a model family used in token_usage with no
  row of its own is inserted from LiteLLM's public price list, never
  updated; off with AGENTHUB_PRICING_SYNC=0. repo-links allows exactly
  that one third-party GitHub raw URL.
2026-09-24 17:05:15 +07:00

101 lines
3.7 KiB
JavaScript

/**
* @file Tests for the automatic pricing sync: a model seen in token_usage with
* no row of its own gets one from the price list, an edited row is never
* overwritten, and a bad list or a failed fetch changes nothing.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const os = require("os");
process.env.DASHBOARD_DB_PATH = path.join(
os.tmpdir(),
`pricing-sync-${Date.now()}-${process.pid}.db`
);
const { db } = require("../db");
const { syncPricing, familyOf, displayName, _resetCache } = require("../lib/pricing-sync");
// The real LiteLLM entry for Opus 5.5, trimmed.
const LIST = {
"claude-opus-9-9": {
litellm_provider: "anthropic",
input_cost_per_token: 0.000004,
output_cost_per_token: 0.00002,
cache_read_input_token_cost: 2e-7,
cache_creation_input_token_cost: 0.000005,
cache_creation_input_token_cost_above_1hr: 0.000008,
supports_fast_mode: true,
provider_specific_entry: { fast: 2 },
},
};
function useModel(model) {
db.prepare("INSERT OR IGNORE INTO sessions (id, status, started_at) VALUES (?, 'active', ?)").run(
`s-${model}`,
new Date().toISOString()
);
db.prepare("INSERT INTO token_usage (session_id, model) VALUES (?, ?)").run(`s-${model}`, model);
}
const row = (p) => db.prepare("SELECT * FROM model_pricing WHERE model_pattern = ?").get(p);
beforeEach(() => {
_resetCache();
db.prepare("DELETE FROM token_usage").run();
db.prepare("DELETE FROM model_pricing WHERE model_pattern LIKE 'claude-opus-9%'").run();
});
describe("pricing sync", () => {
it("reduces an id to its family and names it", () => {
assert.equal(familyOf("claude-opus-5-5-20260101"), "claude-opus-5-5");
assert.equal(familyOf("gpt-4"), null);
assert.equal(familyOf("claude-x%"), null);
assert.equal(displayName("claude-opus-5-5"), "Claude Opus 5.5");
});
it("adds a row for a used model that only a broader pattern matched", async () => {
db.prepare(
"INSERT INTO model_pricing (model_pattern, display_name, input_per_mtok) VALUES ('claude-opus-9%', 'Broad', 5)"
).run();
useModel("claude-opus-9-9-20260101");
const added = await syncPricing(db, { fetchList: async () => LIST });
assert.deepEqual(added, ["claude-opus-9-9%"]);
const r = row("claude-opus-9-9%");
assert.equal(r.display_name, "Claude Opus 9.9");
assert.deepEqual([r.input_per_mtok, r.output_per_mtok, r.cache_read_per_mtok], [4, 20, 0.2]);
assert.deepEqual([r.cache_write_per_mtok, r.cache_write_1h_per_mtok], [5, 8]);
assert.deepEqual([r.fast_input_per_mtok, r.fast_output_per_mtok], [8, 40]);
});
it("never overwrites a row that exists, and fetches nothing when none is missing", async () => {
db.prepare(
"INSERT INTO model_pricing (model_pattern, display_name, input_per_mtok) VALUES ('claude-opus-9-9%', 'Mine', 1)"
).run();
useModel("claude-opus-9-9");
let fetched = false;
await syncPricing(db, {
fetchList: async () => {
fetched = true;
return LIST;
},
});
assert.equal(fetched, false);
assert.equal(row("claude-opus-9-9%").input_per_mtok, 1);
});
it("skips absurd rates and survives a failed fetch", async () => {
useModel("claude-opus-9-9");
const bad = { "claude-opus-9-9": { ...LIST["claude-opus-9-9"], input_cost_per_token: 5 } };
assert.deepEqual(await syncPricing(db, { fetchList: async () => bad }), []);
_resetCache();
const failed = await syncPricing(db, {
fetchList: async () => {
throw new Error("offline");
},
});
assert.deepEqual(failed, []);
assert.equal(row("claude-opus-9-9%"), undefined);
});
});