1import { describe, expect, it } from "vite-plus/test";
2import * as Schema from "effect/Schema";
3
4import { ProviderRuntimeEvent } from "./providerRuntime.ts";
5
6const decodeRuntimeEvent = Schema.decodeUnknownSync(ProviderRuntimeEvent);
7
8describe("ProviderRuntimeEvent", () => {
9 it("requires input and output totals for complete turn usage", () => {
10 const completeEvent = {
11 type: "turn.completed",
12 eventId: "event-complete-usage",
13 provider: "codex",
14 createdAt: "2026-02-28T00:00:00.000Z",
15 threadId: "thread-1",
16 turnId: "turn-1",
17 payload: {
18 state: "completed",
19 tokenUsage: {
20 usageStatus: "complete",
21 usageScope: "main_agent",
22 hasSubagents: false,
23 },
24 },
25 };
26
27 expect(() => decodeRuntimeEvent(completeEvent)).toThrow();
28 expect(
29 decodeRuntimeEvent({
30 ...completeEvent,
31 payload: {
32 ...completeEvent.payload,
33 tokenUsage: {
34 ...completeEvent.payload.tokenUsage,
35 inputTokens: 10,
36 outputTokens: 2,
37 },
38 },
39 }).type,
40 ).toBe("turn.completed");
41 expect(
42 decodeRuntimeEvent({
43 ...completeEvent,
44 payload: {
45 ...completeEvent.payload,
46 tokenUsage: {
47 ...completeEvent.payload.tokenUsage,
48 usageStatus: "partial",
49 },
50 },
51 }).type,
52 ).toBe("turn.completed");
53 });
54
55 it("accepts fork-provided driver kinds as branded slugs", () => {
56 const parsed = decodeRuntimeEvent({
57 type: "session.started",
58 eventId: "event-ollama-session",
59 provider: "ollama",
60 providerInstanceId: "ollama_local",
61 createdAt: "2026-02-28T00:00:00.000Z",
62 threadId: "thread-1",
63 payload: {
64 message: "started",
65 },
66 });
67
68 expect(parsed.provider).toBe("ollama");
69 expect(parsed.providerInstanceId).toBe("ollama_local");
70 });
71
72 it("decodes turn.plan.updated for plan rendering", () => {
73 const parsed = decodeRuntimeEvent({
74 type: "turn.plan.updated",
75 eventId: "event-1",
76 provider: "claudeAgent",
77 sessionId: "runtime-session-1",
78 createdAt: "2026-02-28T00:00:00.000Z",
79 threadId: "thread-1",
80 turnId: "turn-1",
81 payload: {
82 explanation: "Implement schema updates",
83 plan: [
84 { step: "Define event union", status: "completed" },
85 { step: "Wire adapter mapping", status: "inProgress" },
86 ],
87 },
88 });
89
90 expect(parsed.type).toBe("turn.plan.updated");
91 if (parsed.type !== "turn.plan.updated") {
92 throw new Error("expected turn.plan.updated");
93 }
94 expect(parsed.payload.plan).toHaveLength(2);
95 expect(parsed.payload.plan[1]?.status).toBe("inProgress");
96 });
97
98 it("decodes proposed-plan completion events", () => {
99 const parsed = decodeRuntimeEvent({
100 type: "turn.proposed.completed",
101 eventId: "event-proposed-plan-1",
102 provider: "codex",
103 createdAt: "2026-02-28T00:00:00.000Z",
104 threadId: "thread-1",
105 turnId: "turn-1",
106 payload: {
107 planMarkdown: "# Ship it",
108 },
109 });
110
111 expect(parsed.type).toBe("turn.proposed.completed");
112 if (parsed.type !== "turn.proposed.completed") {
113 throw new Error("expected turn.proposed.completed");
114 }
115 expect(parsed.payload.planMarkdown).toBe("# Ship it");
116 });
117
118 it("decodes user-input.requested with structured questions", () => {
119 const parsed = decodeRuntimeEvent({
120 type: "user-input.requested",
121 eventId: "event-2",
122 provider: "claudeAgent",
123 sessionId: "runtime-session-2",
124 createdAt: "2026-02-28T00:00:01.000Z",
125 threadId: "thread-2",
126 requestId: "request-1",
127 payload: {
128 questions: [
129 {
130 id: "sandbox_mode",
131 header: "Sandbox",
132 question: "Which mode should be used?",
133 options: [
134 {
135 label: "workspace-write",
136 description: "Allow edits in workspace only",
137 },
138 {
139 label: "danger-full-access",
140 description: "Allow unrestricted access",
141 },
142 ],
143 },
144 ],
145 },
146 });
147
148 expect(parsed.type).toBe("user-input.requested");
149 if (parsed.type !== "user-input.requested") {
150 throw new Error("expected user-input.requested");
151 }
152 expect(parsed.payload.questions[0]?.id).toBe("sandbox_mode");
153 expect(parsed.payload.questions[0]?.options).toHaveLength(2);
154 });
155
156 it("decodes user-input.resolved with answer map", () => {
157 const parsed = decodeRuntimeEvent({
158 type: "user-input.resolved",
159 eventId: "event-3",
160 provider: "claudeAgent",
161 sessionId: "runtime-session-2",
162 createdAt: "2026-02-28T00:00:02.000Z",
163 threadId: "thread-2",
164 requestId: "request-1",
165 payload: {
166 answers: {
167 sandbox_mode: "workspace-write",
168 },
169 },
170 });
171
172 expect(parsed.type).toBe("user-input.resolved");
173 if (parsed.type !== "user-input.resolved") {
174 throw new Error("expected user-input.resolved");
175 }
176 expect(parsed.payload.answers.sandbox_mode).toBe("workspace-write");
177 });
178
179 it("rejects legacy message.delta type", () => {
180 expect(() =>
181 decodeRuntimeEvent({
182 type: "message.delta",
183 eventId: "event-4",
184 provider: "codex",
185 sessionId: "runtime-session-3",
186 createdAt: "2026-02-28T00:00:03.000Z",
187 payload: { delta: "legacy" },
188 }),
189 ).toThrow();
190 });
191
192 it("rejects empty branded canonical ids", () => {
193 expect(() =>
194 decodeRuntimeEvent({
195 type: "runtime.error",
196 eventId: "event-5",
197 provider: "codex",
198 sessionId: "runtime-session-3",
199 createdAt: "2026-02-28T00:00:03.000Z",
200 threadId: " ",
201 payload: { message: "boom" },
202 }),
203 ).toThrow();
204 });
205
206 it("decodes normalized thread token usage snapshots", () => {
207 const parsed = decodeRuntimeEvent({
208 type: "thread.token-usage.updated",
209 eventId: "event-token-usage-1",
210 provider: "claudeAgent",
211 createdAt: "2026-02-28T00:00:04.000Z",
212 threadId: "thread-1",
213 payload: {
214 usage: {
215 usedTokens: 31251,
216 maxTokens: 200000,
217 toolUses: 25,
218 durationMs: 43567,
219 },
220 },
221 });
222
223 expect(parsed.type).toBe("thread.token-usage.updated");
224 if (parsed.type !== "thread.token-usage.updated") {
225 throw new Error("expected thread.token-usage.updated");
226 }
227 expect(parsed.payload.usage.maxTokens).toBe(200000);
228 expect(parsed.payload.usage.usedTokens).toBe(31251);
229 });
230});