import { describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { BuiltInLLMMessage, Cell, derive, OpaqueRef, recipe, } from "commontools"; describe("derive type inference", () => { // These tests are not meant to run, the test is that they compile correctly. function _doNotRun(): void { it("should unwrap OpaqueRef to T[] in callback", () => { const messages = Cell.of().getAsOpaqueRefProxy(); messages.set([ { role: "user", content: [{ type: "text", text: "hello" }] }, { role: "assistant", content: [{ type: "text", text: "hi" }] }, ]); const assistantCount = derive(messages, (msgs) => { // Type assertion to verify the type inference is correct // If this compiles, it means msgs is properly typed as Message[] const _typeCheck: BuiltInLLMMessage[] = msgs; return msgs.filter((m) => m.role === "assistant").length; }); expect(assistantCount).toBeDefined(); }); it("should unwrap nested object types in OpaqueRef", () => { interface ComplexMessage { role: "user" | "assistant" | "system"; content: string | { text: string; type: string }[]; } const messages = Cell.of().getAsOpaqueRefProxy(); messages.set([ { role: "assistant", content: "hello" }, { role: "user", content: [{ text: "hi", type: "text" }], }, ]); const lastMessage = derive(messages, (msgs) => { // Verify we can access array properties and methods if (!msgs || msgs.length === 0) return null; const last = msgs[msgs.length - 1]; // Verify we can access nested properties with proper types const content = typeof last.content === "string" ? last.content : last.content.map((part) => part.text).join(""); return { role: last.role, content }; }); expect(lastMessage).toBeDefined(); }); it("should handle primitive types", () => { const number = Cell.of().getAsOpaqueRefProxy(); const boolean = Cell.of().getAsOpaqueRefProxy(); const string = Cell.of().getAsOpaqueRefProxy(); const derivedNumber = derive(number, (num) => { // Type check: nums should be number[] const _typeCheck: number = num; return num + 1; }); const derivedBoolean = derive(boolean, (bool) => { const _typeCheck: boolean = bool; return !bool; }); const derivedString = derive(string, (str) => { const _typeCheck: string = str; return str + "!"; }); expect(derivedNumber).toBeDefined(); expect(derivedBoolean).toBeDefined(); expect(derivedString).toBeDefined(); }); it("should handle primitive array types", () => { const numbers = Cell.of().getAsOpaqueRefProxy(); numbers.set([1, 2, 3, 4, 5]); const sum = derive(numbers, (nums) => { // Type check: nums should be number[] const _typeCheck: number[] = nums; return nums.reduce((acc, n) => acc + n, 0); }); expect(sum).toBeDefined(); }); it("should handle nested array types", () => { const matrix = Cell.of().getAsOpaqueRefProxy(); matrix.set([[1, 2], [3, 4], [5, 6]]); const flattened = derive(matrix, (m) => { // Type check: m should be number[][] const _typeCheck: number[][] = m; return m.flat(); }); expect(flattened).toBeDefined(); }); it("should handle object with nested properties", () => { interface User { member: boolean; name: string; email: string; profile: { age: number; city: string; }; } const user = Cell.of().getAsOpaqueRefProxy(); user.set({ name: "Alice", email: "alice@example.com", profile: { age: 30, city: "NYC" }, }); const displayName = derive(user, (u) => { // Type check: u should be User const _typeCheck: User = u; const _member: boolean = u.member; return `${u.name} (${u.profile.city})`; }); expect(displayName).toBeDefined(); }); it("should handle object with nested properties", () => { interface User { name: string; email: string; profile: { age: number; city: string; }; } const user = Cell.of().getAsOpaqueRefProxy(); user.set({ name: "Alice", email: "alice@example.com", profile: { age: 30, city: "NYC" }, }); const displayName = derive({ user }, ({ user }) => { // Type check: u should be User const _typeCheck: User = user; return `${user.name} (${user.profile.city})`; }); expect(displayName).toBeDefined(); }); it("should unwrap sub-properties of OpaqueRef (like omnibot.messages)", () => { interface Message { role: "user" | "assistant" | "system"; content: string; } interface ChatbotState { messages: Message[]; system: string; pending: boolean; } const chatbot = Cell.of().getAsOpaqueRefProxy(); chatbot.set({ messages: [ { role: "user", content: "hello" }, { role: "assistant", content: "hi there" }, { role: "user", content: "how are you?" }, { role: "assistant", content: "I'm doing well!" }, ], system: "You are a helpful assistant", pending: false, }); // This simulates the exact pattern from omnibox-fab.tsx: // derive(omnibot.messages, (messages) => ...) const assistantCount = derive(chatbot.messages, (messages) => { // Type check: messages should be Message[], not wrapped const _typeCheck: Message[] = messages; return messages.filter((m) => m.role === "assistant").length; }); expect(assistantCount).toBeDefined(); // Also test the other derive from omnibox-fab.tsx const latestAssistantMessage = derive(chatbot.messages, (messages) => { // Type check: messages should be Message[] const _typeCheck: Message[] = messages; if (!messages || messages.length === 0) return null; for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.role === "assistant") { return msg.content; } } return null; }); expect(latestAssistantMessage).toBeDefined(); }); it("should handle the exact type structure from recipe return values", () => { // This tests chatbot.messages where chatbot is OpaqueRef // and messages is accessed as a property type Message = { role: "user" | "assistant"; content: string; }; type ChatOutput = { messages: Message[]; pending: boolean; }; // Simulate what happens when you call a recipe const chatbot = Cell.of().getAsOpaqueRefProxy(); chatbot.set({ messages: [ { role: "user", content: "hello" }, { role: "assistant", content: "response" }, ], pending: false, }); // Access messages property - this has type OpaqueCell & Array> const messages = chatbot.messages; // Log the actual type to understand what we're dealing with type MessagesType = typeof messages; // MessagesType should be: OpaqueCell & Array> // This is the exact pattern: derive(omnibot.messages, (messages) => ...) const result = derive(messages, (msgs) => { // This should compile without errors - msgs should be Message[] // This was the bug: msgs was being typed as the complex intersection type // Now it should be correctly unwrapped to Message[] const _typeCheck: Message[] = msgs; return msgs.filter((m) => m.role === "assistant").length; }); expect(result).toBeDefined(); }); it("should handle array intersection types (OpaqueCell & Array>)", () => { // This explicitly tests the intersection type that occurs with sub-properties interface Item { id: number; name: string; } const parent = Cell.of<{ items: Item[] }>().getAsOpaqueRefProxy(); parent.set({ items: [ { id: 1, name: "first" }, { id: 2, name: "second" }, ], }); // parent.items has type: OpaqueCell & Array> const items = parent.items; // The derive callback should receive Item[], not the complex wrapped type const count = derive(items, (itemsList) => { const _typeCheck: Item[] = itemsList; return itemsList.length; }); expect(count).toBeDefined(); }); it("should handle recipe return values with array properties (actual omnibot.messages case)", () => { // This reproduces the ACTUAL bug from omnibox-fab.tsx interface ChatbotInput { initialMessage?: string; } interface ChatbotOutput { messages: BuiltInLLMMessage[]; } // Create a recipe that returns an object with an array property const Chatbot = recipe("TestChatbot", () => { const messagesRef = Cell.of() .getAsOpaqueRefProxy(); messagesRef.set([ { role: "user", content: "hello" }, { role: "assistant", content: "response" }, ]); return { messages: messagesRef, }; }); // Call the recipe - this is like `const omnibot = Chatbot(...)` const omnibot = Chatbot({}); // Access the messages property - this is like `omnibot.messages` // This is where the type becomes Opaque[] instead of Message[] const assistantCount = derive(omnibot.messages, (messages) => { // This should be Message[], not Opaque[] const _typeCheck: BuiltInLLMMessage[] = messages; return messages.filter((m) => m.role === "assistant").length; }); expect(assistantCount).toBeDefined(); }); it("should support destructuring derive inputs for nested properties", () => { const Chatbot = recipe< Record, { messages: BuiltInLLMMessage[] } >( "ChatbotWithMessages", () => { const messagesRef = Cell.of() .getAsOpaqueRefProxy(); messagesRef.set([ { role: "user", content: "hello" }, { role: "assistant", content: "response" }, ]); return { messages: messagesRef, }; }, ); const omnibot = Chatbot({}); const assistantCount = derive( { messages: omnibot.messages }, ({ messages }) => { const _typeCheck: BuiltInLLMMessage[] = messages; return messages.filter((m) => m.role === "assistant").length; }, ); expect(assistantCount).toBeDefined(); }); describe("derive with Cell inputs", () => { interface UserProfile { name: string; active: boolean; } const profileCell = Cell.of(); profileCell.set({ name: "Ada", active: true }); it("should unwrap Cell.of inputs directly", () => { const result = derive(profileCell, (profile) => { const _typeCheck: Cell = profile; return profile === profile ? 1 : 0; }); expect(result).toBeDefined(); }); it("should unwrap OpaqueRef> inputs", () => { const profileCellRef = profileCell as unknown as OpaqueRef< Cell >; const isActive = derive(profileCellRef, (profile) => { const _typeCheck: Cell = profile; return profile; }); expect(isActive).toBeDefined(); }); it("should unwrap destructured objects containing Cell", () => { const derived = derive({ profile: profileCell }, ({ profile }) => { const _typeCheck: Cell = profile; return profile; }); expect(derived).toBeDefined(); }); it("should unwrap destructured OpaqueRef objects containing Cell", () => { const container = { profile: profileCell } as OpaqueRef< { profile: Cell } >; const derived = derive(container, ({ profile }) => { const _typeCheck: Cell = profile; return profile; }); expect(derived).toBeDefined(); }); }); it("should honor explicit derive typing", () => { type ExplicitInput = { role: "user"; text: string }; const explicitCell = Cell.of().getAsOpaqueRefProxy(); explicitCell.set({ role: "user", text: "hello" }); const derived = derive( explicitCell, (value) => { const _typeCheck: ExplicitInput = value; return value.text.length; }, ); expect(derived).toBeDefined(); }); it("should honor explicit parameter typing", () => { type ExplicitInput = { role: "user"; text: string }; const explicitCell = Cell.of().getAsOpaqueRefProxy(); explicitCell.set({ role: "user", text: "hello" }); const derived = derive( explicitCell, (value: ExplicitInput) => { const _typeCheck: ExplicitInput = value; return value.text.length; }, ); expect(derived).toBeDefined(); }); it("should honor explicit Cell inputs", () => { type ExplicitInput = { role: "user"; text: string }; const explicitCell = Cell.of>().getAsOpaqueRefProxy(); explicitCell.set({ role: "user", text: "hello" }); const derived = derive( explicitCell, (value: Cell) => { const _typeCheck: Cell = value; return value.get().text.length; }, ); expect(derived).toBeDefined(); }); } });