1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4
5import { ModelSelection } from "./modelSelection.ts";
6import { ProviderInstanceId } from "./providerInstance.ts";
7
8// ── ModelSelection: instance-keyed wire shape + legacy decoder ────────
9//
10// `ModelSelection` is routing-keyed on `instanceId` — never a driver kind.
11// Persisted and in-flight payloads from pre-instance builds carry a
12// `provider` field whose value was a driver kind; those payloads are migrated
13// at the wire boundary by
14// promoting `provider` to the default instance id for that driver
15// (built-in drivers use the driver kind slug as their default instance id, so
16// the migration is a 1:1 rename).
17//
18// These tests pin the rollback/fork tolerance invariant: legacy payloads
19// decode cleanly for fork-provided drivers, and the decoded form uses
20// `instanceId` uniformly regardless of origin.
21
22const decodeModelSelection = Schema.decodeUnknownEffect(ModelSelection);
23const encodeModelSelection = Schema.encodeUnknownEffect(ModelSelection);
24
25it.effect("ModelSelection migrates legacy `provider` field to `instanceId`", () =>
26 Effect.gen(function* () {
27 const parsed = yield* decodeModelSelection({
28 provider: "codex",
29 model: "gpt-5-codex",
30 options: [{ id: "reasoningEffort", value: "high" }],
31 });
32 assert.strictEqual(parsed.instanceId, ProviderInstanceId.make("codex"));
33 assert.strictEqual(parsed.model, "gpt-5-codex");
34 assert.deepStrictEqual(parsed.options, [{ id: "reasoningEffort", value: "high" }]);
35 }),
36);
37
38it.effect("ModelSelection accepts an explicit instanceId routing key", () =>
39 Effect.gen(function* () {
40 const parsed = yield* decodeModelSelection({
41 instanceId: "codex_personal",
42 model: "gpt-5-codex",
43 });
44 assert.strictEqual(parsed.instanceId, ProviderInstanceId.make("codex_personal"));
45 }),
46);
47
48it.effect("ModelSelection prefers explicit instanceId over legacy provider", () =>
49 Effect.gen(function* () {
50 const parsed = yield* decodeModelSelection({
51 provider: "codex",
52 instanceId: "codex_personal",
53 model: "gpt-5-codex",
54 });
55 assert.strictEqual(parsed.instanceId, ProviderInstanceId.make("codex_personal"));
56 }),
57);
58
59it.effect(
60 "ModelSelection decodes unknown driver kinds via legacy provider (rollback / fork invariant)",
61 () =>
62 Effect.gen(function* () {
63 const parsed = yield* decodeModelSelection({
64 provider: "ollama",
65 model: "llama3:70b",
66 options: [{ id: "temperature", value: "0.4" }],
67 });
68 assert.strictEqual(parsed.instanceId, ProviderInstanceId.make("ollama"));
69 assert.strictEqual(parsed.model, "llama3:70b");
70 }),
71);
72
73it.effect("ModelSelection encodes to the canonical instanceId wire form", () =>
74 Effect.gen(function* () {
75 const decoded = yield* decodeModelSelection({
76 provider: "ollama",
77 model: "llama3:70b",
78 options: [{ id: "temperature", value: "0.4" }],
79 });
80 const encoded = yield* encodeModelSelection(decoded);
81 assert.deepStrictEqual(encoded, {
82 instanceId: "ollama",
83 model: "llama3:70b",
84 options: [{ id: "temperature", value: "0.4" }],
85 });
86 }),
87);
88
89it.effect("ModelSelection rejects malformed instance ids", () =>
90 Effect.gen(function* () {
91 const result = yield* Effect.exit(
92 decodeModelSelection({
93 instanceId: "1invalid", // must start with a letter
94 model: "x",
95 }),
96 );
97 assert.strictEqual(result._tag, "Failure");
98 }),
99);
100
101it.effect(
102 "normalizes legacy object-shaped options on decode and re-encodes as a canonical array",
103 () =>
104 Effect.gen(function* () {
105 const decoded = yield* decodeModelSelection({
106 provider: "claudeAgent",
107 model: "claude-opus-4-6",
108 options: {
109 effort: "max",
110 fastMode: true,
111 // Falsy/garbage entries are dropped, matching migration 026.
112 emptyStr: " ",
113 nullish: null,
114 nested: { foo: 1 },
115 },
116 });
117 assert.deepStrictEqual(decoded.options, [
118 { id: "effort", value: "max" },
119 { id: "fastMode", value: true },
120 ]);
121 const encoded = yield* encodeModelSelection(decoded);
122 assert.deepStrictEqual(encoded.options, decoded.options);
123 }),
124);