1import { EnvironmentId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3import {
4 usageAliasCell,
5 usagePriceCell,
6 usagePriceTableChanges,
7 usagePriceTableErrors,
8 type UsagePriceDraft,
9} from "./usagePriceTable";
10import type { UsagePriceTarget } from "./usagePriceTargets";
11
12const price = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 };
13const target = (
14 name: string,
15 prices: UsagePriceTarget["prices"],
16 aliases: UsagePriceTarget["aliases"] = {},
17): UsagePriceTarget => ({
18 environmentId: EnvironmentId.make(name),
19 label: name,
20 prices,
21 aliases,
22 unavailable: null,
23});
24const draft = (values: UsagePriceDraft["values"]): UsagePriceDraft => ({
25 id: "model:example",
26 model: "example",
27 isNew: false,
28 values,
29});
30
31describe("price table edits", () => {
32 it("shows shared and mixed cells separately, and never treats missing settings as automatic", () => {
33 const targets = [
34 target("a", { example: price }),
35 target("b", { example: { ...price, outputCostPerMillionTokens: 10 } }),
36 ];
37 expect(usagePriceCell(targets, "example", "inputCostPerMillionTokens").value).toBe("2");
38 expect(usagePriceCell(targets, "example", "outputCostPerMillionTokens")).toEqual({
39 value: "",
40 placeholder: "Mixed",
41 });
42 expect(usagePriceCell(targets, "example", "cacheReadCostPerMillionTokens")).toEqual({
43 value: "",
44 placeholder: "Input rate",
45 });
46 expect(
47 usagePriceCell([target("a", {})], "example", "inputCostPerMillionTokens").placeholder,
48 ).toBe("Automatic");
49 expect(
50 usagePriceCell([targets[0]!, target("b", null)], "example", "inputCostPerMillionTokens")
51 .placeholder,
52 ).toBe("Unavailable");
53 });
54
55 it.each(["constructor", "toString", "__proto__"])(
56 "handles an exact model ID named %s",
57 (model) => {
58 const environment = target("a", {});
59 expect(usagePriceCell([environment], model, "inputCostPerMillionTokens")).toEqual({
60 value: "",
61 placeholder: "Automatic",
62 });
63 const result = usagePriceTableChanges(environment, [
64 {
65 ...draft({ inputCostPerMillionTokens: "2", outputCostPerMillionTokens: "8" }),
66 model,
67 isNew: true,
68 },
69 ]);
70 expect(result.changes).toEqual([{ model, price }]);
71 expect(
72 usagePriceCell([target("a", { [model]: price })], model, "inputCostPerMillionTokens").value,
73 ).toBe("2");
74 },
75 );
76
77 it("changes only edited columns while preserving each environment's other prices and models", () => {
78 const edits = [draft({ inputCostPerMillionTokens: "3" })];
79 const a = usagePriceTableChanges(
80 target("a", { example: { ...price, cacheReadCostPerMillionTokens: 0 }, untouched: price }),
81 edits,
82 );
83 const b = usagePriceTableChanges(
84 target("b", { example: { ...price, outputCostPerMillionTokens: 10 } }),
85 edits,
86 );
87 expect(a.changes).toEqual([
88 {
89 model: "example",
90 price: { ...price, inputCostPerMillionTokens: 3, cacheReadCostPerMillionTokens: 0 },
91 },
92 ]);
93 expect(b.changes).toEqual([
94 { model: "example", price: { inputCostPerMillionTokens: 3, outputCostPerMillionTokens: 10 } },
95 ]);
96 });
97
98 it("batches new rows and resets, and distinguishes blank cache rates from explicit zero", () => {
99 const plan = usagePriceTableChanges(target("a", { example: price }), [
100 { ...draft({}), removed: true },
101 {
102 id: "new:1",
103 model: " vendor/Model ",
104 isNew: true,
105 values: {
106 inputCostPerMillionTokens: "2",
107 outputCostPerMillionTokens: "8",
108 cacheReadCostPerMillionTokens: "0",
109 cacheWriteCostPerMillionTokens: "",
110 },
111 },
112 ]);
113 expect(plan.errors.size).toBe(0);
114 expect(plan.changes).toEqual([
115 { model: "example", price: null },
116 { model: "vendor/Model", price: { ...price, cacheReadCostPerMillionTokens: 0 } },
117 ]);
118 });
119
120 it("requires complete prices before turning automatic pricing into an override", () => {
121 const edits = [draft({ inputCostPerMillionTokens: "3" })];
122 expect(usagePriceTableChanges(target("a", { example: price }), edits).errors.size).toBe(0);
123 const missing = usagePriceTableChanges(target("b", {}), edits);
124 expect(missing.changes).toEqual([]);
125 expect(missing.errors.has("model:example")).toBe(true);
126 });
127
128 it("ignores empty new rows without blocking other edits", () => {
129 const environment = target("a", { example: price });
130 const empty: UsagePriceDraft = {
131 id: "new:1",
132 model: " ",
133 isNew: true,
134 values: { inputCostPerMillionTokens: " " },
135 };
136 const plan = usagePriceTableChanges(environment, [
137 draft({ inputCostPerMillionTokens: "3" }),
138 empty,
139 ]);
140 expect(plan.errors.size).toBe(0);
141 expect(plan.changes).toEqual([
142 { model: "example", price: { ...price, inputCostPerMillionTokens: 3 } },
143 ]);
144 expect(usagePriceTableChanges(environment, [empty]).changes).toEqual([]);
145 expect(
146 usagePriceTableChanges(environment, [
147 { ...empty, values: { inputCostPerMillionTokens: "0" } },
148 ]).errors.get(empty.id),
149 ).toBe("Enter a model ID.");
150 });
151
152 it.each(["Offline", "Read-only access", "Update server to edit prices"])(
153 "does not let %s destinations block a valid edit elsewhere",
154 (unavailable) => {
155 const edits = [draft({ inputCostPerMillionTokens: "3" })];
156 const writable = target("writable", { example: price });
157 const other = target("other", {});
158 expect(usagePriceTableErrors([writable, { ...other, unavailable }], edits).size).toBe(0);
159 expect(usagePriceTableErrors([writable, other], edits).has("model:example")).toBe(true);
160 },
161 );
162
163 it("does not write unchanged rates and rejects invalid edited cells", () => {
164 const environment = target("a", { example: price });
165 expect(
166 usagePriceTableChanges(environment, [draft({ inputCostPerMillionTokens: "2.00" })]).changes,
167 ).toEqual([]);
168 expect(
169 usagePriceTableChanges(environment, [draft({ cacheReadCostPerMillionTokens: "-1" })]).errors
170 .size,
171 ).toBe(1);
172 expect(
173 usagePriceTableChanges(environment, [draft({ outputCostPerMillionTokens: "" })]).errors.size,
174 ).toBe(1);
175 });
176
177 it("mapping a model drops its own price and resetting removes the mapping", () => {
178 const priced = target("a", { example: price });
179 expect(usagePriceTableChanges(priced, [{ ...draft({}), alias: " example-model " }])).toEqual({
180 changes: [
181 { model: "example", alias: "example-model" },
182 { model: "example", price: null },
183 ],
184 errors: new Map(),
185 });
186 const mapped = target("b", {}, { example: "example-model" });
187 expect(usagePriceTableChanges(mapped, [{ ...draft({}), removed: true }]).changes).toEqual([
188 { model: "example", alias: null },
189 ]);
190 expect(
191 usagePriceTableChanges(mapped, [{ ...draft({}), alias: "example" }]).errors.get(
192 "model:example",
193 ),
194 ).toBe("Map to a different model.");
195 });
196
197 it("clearing a mapping returns to automatic pricing unless prices are entered", () => {
198 const mapped = target("a", {}, { example: "example-model" });
199 expect(usagePriceTableChanges(mapped, [{ ...draft({}), alias: "" }]).changes).toEqual([
200 { model: "example", alias: null },
201 ]);
202 expect(
203 usagePriceTableChanges(mapped, [
204 {
205 ...draft({ inputCostPerMillionTokens: "2", outputCostPerMillionTokens: "8" }),
206 alias: "",
207 },
208 ]).changes,
209 ).toEqual([
210 { model: "example", alias: null },
211 { model: "example", price },
212 ]);
213 });
214
215 it("keeps each environment's mapping when the column is untouched", () => {
216 const edits = [draft({ inputCostPerMillionTokens: "3" })];
217 expect(
218 usagePriceTableChanges(target("a", {}, { example: "example-model" }), edits).changes,
219 ).toEqual([]);
220 expect(
221 usageAliasCell(
222 [target("a", {}, { example: "example-model" }), target("b", { example: price })],
223 "example",
224 ),
225 ).toEqual({ value: "", placeholder: "Mixed" });
226 });
227});