1import { EnvironmentId } from "@t3tools/contracts";
2import { describe, expect, it, vi } from "vite-plus/test";
3
4import {
5 writeUsagePrices,
6 type UsagePriceTarget,
7 type UsagePriceWriteResult,
8} from "./usagePriceTargets";
9
10const price = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 };
11const target = (id: string, overrides: Partial<UsagePriceTarget> = {}): UsagePriceTarget => ({
12 environmentId: EnvironmentId.make(id),
13 label: id,
14 prices: { example: price },
15 aliases: {},
16 unavailable: null,
17 ...overrides,
18});
19
20describe("model price writes", () => {
21 it("sends all table changes in one patch per environment and skips unchanged destinations", async () => {
22 const write = vi.fn(async () => ({ _tag: "Success" as const }));
23 const onResult = vi.fn();
24 await writeUsagePrices({
25 targets: [target("edited"), target("unchanged")],
26 changes: new Map([
27 [
28 EnvironmentId.make("edited"),
29 [
30 { model: "new-model", price },
31 { model: "example", price: null },
32 ],
33 ],
34 ]),
35 write,
36 onResult,
37 });
38 expect(write).toHaveBeenCalledExactlyOnceWith({
39 environmentId: "edited",
40 input: { patch: { usagePriceOverrides: { "new-model": price, example: null } } },
41 });
42 expect(onResult).toHaveBeenCalledWith("unchanged", { status: "saved" });
43 });
44
45 it("saves independently, skips unavailable targets, and retries failures without rewriting successes", async () => {
46 let resolveSlow!: (result: { _tag: "Success" | "Failure" }) => void;
47 const slow = new Promise<{ _tag: "Success" | "Failure" }>((resolve) => {
48 resolveSlow = resolve;
49 });
50 let resolveFastSaved!: () => void;
51 const fastSaved = new Promise<void>((resolve) => {
52 resolveFastSaved = resolve;
53 });
54 const targets = [target("fast"), target("slow"), target("offline", { unavailable: "Offline" })];
55 const results = new Map<EnvironmentId, UsagePriceWriteResult>();
56 const write = vi.fn(async ({ environmentId }: { environmentId: EnvironmentId }) =>
57 environmentId === "slow" ? slow : { _tag: "Success" as const },
58 );
59 const change = { model: "example", price };
60 const running = writeUsagePrices({
61 targets,
62 changes: new Map(targets.map((target) => [target.environmentId, [change]])),
63 write,
64 onResult: (id, result) => {
65 results.set(id, result);
66 if (id === "fast") resolveFastSaved();
67 },
68 });
69 await fastSaved;
70 expect(results.get(EnvironmentId.make("fast"))).toEqual({ status: "saved" });
71 expect(results.get(EnvironmentId.make("offline"))).toEqual({
72 status: "failed",
73 error: "Offline",
74 });
75 expect(results.has(EnvironmentId.make("slow"))).toBe(false);
76 resolveSlow({ _tag: "Failure" });
77 await running;
78 expect(write).toHaveBeenCalledTimes(2);
79 expect(write).toHaveBeenCalledWith({
80 environmentId: "fast",
81 input: { patch: { usagePriceOverrides: { example: price } } },
82 });
83 const retry = vi.fn(async () => ({ _tag: "Success" as const }));
84 await writeUsagePrices({
85 targets: targets
86 .filter((entry) => results.get(entry.environmentId)?.status === "failed")
87 .map((entry) => ({ ...entry, unavailable: null })),
88 changes: new Map(targets.map((target) => [target.environmentId, [change]])),
89 write: retry,
90 onResult: (id, result) => {
91 results.set(id, result);
92 },
93 });
94 expect(retry.mock.calls).toHaveLength(2);
95 expect(retry).not.toHaveBeenCalledWith(expect.objectContaining({ environmentId: "fast" }));
96 expect([...results.values()].every((result) => result.status === "saved")).toBe(true);
97 });
98
99 it("resets only the chosen model on the selected destination", async () => {
100 const write = vi.fn(async () => ({ _tag: "Success" as const }));
101 const onResult = vi.fn();
102 await writeUsagePrices({
103 targets: [target("selected")],
104 changes: new Map([[EnvironmentId.make("selected"), [{ model: "example", price: null }]]]),
105 write,
106 onResult,
107 });
108 expect(write).toHaveBeenCalledExactlyOnceWith({
109 environmentId: "selected",
110 input: { patch: { usagePriceOverrides: { example: null } } },
111 });
112 expect(onResult).toHaveBeenCalledWith("selected", { status: "saved" });
113 });
114
115 it("reports thrown writes and permission/version restrictions without aborting other saves", async () => {
116 const onResult = vi.fn();
117 const write = vi.fn(async () => {
118 throw new Error("connection lost");
119 });
120 await writeUsagePrices({
121 targets: [
122 target("lost"),
123 target("denied", { unavailable: "Read-only access" }),
124 target("old", { unavailable: "Update server to edit prices" }),
125 ],
126 changes: new Map([[EnvironmentId.make("lost"), [{ model: "example", price }]]]),
127 write,
128 onResult,
129 });
130 expect(write).toHaveBeenCalledTimes(1);
131 expect(onResult).toHaveBeenCalledWith("lost", {
132 status: "failed",
133 error: "Could not save. Try again.",
134 });
135 expect(onResult).toHaveBeenCalledWith("denied", {
136 status: "failed",
137 error: "Read-only access",
138 });
139 expect(onResult).toHaveBeenCalledWith("old", {
140 status: "failed",
141 error: "Update server to edit prices",
142 });
143 });
144
145 it("sends mappings beside prices and fails servers that cannot store them", async () => {
146 const write = vi.fn(async () => ({ _tag: "Success" as const }));
147 const onResult = vi.fn();
148 const changes = [
149 { model: "example", alias: "example-model" },
150 { model: "example", price: null },
151 ];
152 await writeUsagePrices({
153 targets: [target("current"), target("old", { aliases: null })],
154 changes: new Map([
155 [EnvironmentId.make("current"), changes],
156 [EnvironmentId.make("old"), changes],
157 ]),
158 write,
159 onResult,
160 });
161 expect(write).toHaveBeenCalledExactlyOnceWith({
162 environmentId: "current",
163 input: {
164 patch: {
165 usagePriceOverrides: { example: null },
166 usageModelAliases: { example: "example-model" },
167 },
168 },
169 });
170 expect(onResult).toHaveBeenCalledWith("old", {
171 status: "failed",
172 error: "Update server to map models",
173 });
174 });
175});