Skip to content
Source
Sign inSign up

modelSwitch.test.ts

107 lines3.3 KB

1import {
2 ContextHandoffId,
3 MessageId,
4 ProviderInstanceId,
5 type OrchestrationV2ThreadProjection,
6} from "@t3tools/contracts";
7import { describe, expect, it } from "vite-plus/test";
8​
9import { modelSwitchSince } from "./modelSwitch.ts";
10​
11const instanceId = ProviderInstanceId.make("codex");
12const selection = (model: string) => ({ instanceId, model });
13const run = (
14 message: string,
15 ordinal: number,
16 model: string,
17 status: OrchestrationV2ThreadProjection["runs"][number]["status"] = "completed",
18) =>
19 ({
20 ordinal,
21 status,
22 completedAt: null,
23 contextHandoffId: null,
24 userMessageId: MessageId.make(message),
25 modelSelection: selection(model),
26 }) as OrchestrationV2ThreadProjection["runs"][number];
27const shown = (...messages: ReadonlyArray<string>) =>
28 messages.map((message) => ({
29 type: "user_message",
30 messageId: MessageId.make(message),
31 inputIntent: "turn_start",
32 status: "completed",
33 text: message,
34 attachments: [],
35 })) as unknown as OrchestrationV2ThreadProjection["turnItems"];
36​
37describe("modelSwitchSince", () => {
38 it("measures from the latest run whose message is in the timeline", () => {
39 // B failed before it started; its message shows "a -> b", so the next one must show "b -> a".
40 expect(
41 modelSwitchSince(
42 {
43 runs: [
44 run("first", 1, "a"),
45 run("failed", 2, "b", "failed"),
46 run("queued", 3, "c", "queued"),
47 run("undone", 4, "d", "rolled_back"),
48 ],
49 turnItems: shown("first", "failed", "queued", "undone"),
50 },
51 selection("a"),
52 ),
53 ).toEqual({ modelSwitch: { from: selection("b"), to: selection("a") } });
54 });
55​
56 it("skips messages clients hide", () => {
57 const [first, pending, compact, answer] = shown(
58 "first",
59 "pending",
60 "/compact",
61 "async-answer:question",
62 );
63 expect(
64 modelSwitchSince(
65 {
66 runs: [
67 run("first", 1, "a"),
68 run("pending", 2, "b", "cancelled"),
69 run("/compact", 3, "c"),
70 run("async-answer:question", 4, "d"),
71 ],
72 turnItems: [
73 first!,
74 { ...pending!, inputIntent: "queued_turn", status: "pending" },
75 compact!,
76 answer!,
77 ] as OrchestrationV2ThreadProjection["turnItems"],
78 },
79 selection("c"),
80 ),
81 ).toEqual({ modelSwitch: { from: selection("a"), to: selection("c") } });
82 });
83​
84 it("counts a wake whose handoff row shows the swap", () => {
85 const wake = {
86 ...run("wake", 2, "other"),
87 contextHandoffId: ContextHandoffId.make("handoff"),
88 };
89 expect(
90 modelSwitchSince(
91 { runs: [run("first", 1, "a"), wake], turnItems: shown("first") },
92 selection("other"),
93 ),
94 ).toEqual({});
95 });
96​
97 it("records nothing for a first run or an unchanged selection", () => {
98 expect(modelSwitchSince({ runs: [], turnItems: [] }, selection("a"))).toEqual({});
99 // A fork's first message compares with the run it forked from.
100 expect(modelSwitchSince({ runs: [], turnItems: [] }, selection("a"), selection("b"))).toEqual({
101 modelSwitch: { from: selection("b"), to: selection("a") },
102 });
103 expect(
104 modelSwitchSince({ runs: [run("first", 1, "a")], turnItems: shown("first") }, selection("a")),
105 ).toEqual({});
106 });
107});