| 1 | import { EnvironmentId } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 3 | |
| 4 | import { |
| 5 | writeUsagePrices, |
| 6 | type UsagePriceTarget, |
| 7 | type UsagePriceWriteResult, |
| 8 | } from "./usagePriceTargets"; |
| 9 | |
| 10 | const price = { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 }; |
| 11 | const 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 | |
| 20 | describe("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 | }); |