/** * Property-based tests for saved response fork isolation. * * **Validates: Requirements 7.5** * * Property 9: Saved response fork isolation — generate arbitrary SavedResponse * objects, fork into new ChatState; assert messages equals messages_pair and * conversationId is a new UUID distinct from source_conversation_id. */ import { describe, it, expect } from "vitest"; import * as fc from "fast-check"; import { forkSavedResponse } from "./fork"; import type { Message, QueryResult, SavedResponse } from "../types"; // --------------------------------------------------------------------------- // Arbitraries // --------------------------------------------------------------------------- /** ISO 8601 UTC timestamp arbitrary */ const isoTimestamp = fc .date({ min: new Date("2020-01-01"), max: new Date("2030-01-01") }) .map((d) => d.toISOString()); /** Arbitrary Message */ const messageArb: fc.Arbitrary = fc.record({ role: fc.constantFrom("user" as const, "assistant" as const), content: fc.string({ minLength: 0, maxLength: 500 }), }); /** Arbitrary QueryResult */ const queryResultArb: fc.Arbitrary = fc.record({ citations: fc.array( fc.record({ index: fc.nat(), domain: fc.string({ minLength: 1, maxLength: 50 }), title: fc.string({ minLength: 1, maxLength: 100 }), source_url: fc.option(fc.webUrl(), { nil: undefined }), line_num: fc.option(fc.nat(), { nil: undefined }), }), { maxLength: 5 } ), domains_searched: fc.array(fc.string({ minLength: 1, maxLength: 50 }), { maxLength: 5, }), sections_retrieved: fc.nat({ max: 20 }), timing: fc.dictionary( fc.string({ minLength: 1, maxLength: 20 }), fc.float({ noNaN: true, noDefaultInfinity: true }).filter((v) => !Object.is(v, -0)) ), token_usage: fc.record({ total_tokens: fc.nat({ max: 10000 }) }), }); /** Arbitrary SavedResponse */ const savedResponseArb: fc.Arbitrary = fc .tuple( fc.uuid(), // source_conversation_id fc.nat({ max: 100 }), // msg_index fc.string({ minLength: 1, maxLength: 200 }), // question fc.string({ minLength: 1, maxLength: 2000 }), // answer isoTimestamp, // saved_at fc.string({ minLength: 1, maxLength: 80 }), // source_conversation_title fc.array(messageArb, { minLength: 2, maxLength: 2 }), // messages_pair fc.array(messageArb, { minLength: 2, maxLength: 2 }), // history_pair queryResultArb // result ) .map( ([ source_conversation_id, msg_index, question, answer, saved_at, source_conversation_title, messages_pair, history_pair, result, ]) => ({ id: `${source_conversation_id}:${msg_index}`, question, answer, saved_at, source_conversation_id, source_conversation_title, msg_index, messages_pair, history_pair, result, }) ); // --------------------------------------------------------------------------- // Property 9: Saved response fork isolation // --------------------------------------------------------------------------- describe("Property 9: Saved response fork isolation", () => { /** * **Validates: Requirements 7.5** * * For any SavedResponse, forking it into a new ChatState produces a session * whose messages contains exactly the two messages in messages_pair and whose * conversationId is a new UUID distinct from source_conversation_id. */ it("forked ChatState messages equals messages_pair from the SavedResponse", () => { fc.assert( fc.property(savedResponseArb, fc.uuid(), (sr, newId) => { const chatState = forkSavedResponse(sr, newId); expect(chatState.messages).toEqual(sr.messages_pair); }) ); }); it("forked ChatState conversationId is the new UUID, distinct from source_conversation_id", () => { fc.assert( fc.property( savedResponseArb, fc.uuid().filter((_id) => true), // any UUID (sr, newId) => { // Ensure the new ID is distinct from source_conversation_id fc.pre(newId !== sr.source_conversation_id); const chatState = forkSavedResponse(sr, newId); expect(chatState.conversationId).toBe(newId); expect(chatState.conversationId).not.toBe(sr.source_conversation_id); } ) ); }); it("forked ChatState history equals history_pair from the SavedResponse", () => { fc.assert( fc.property(savedResponseArb, fc.uuid(), (sr, newId) => { const chatState = forkSavedResponse(sr, newId); expect(chatState.history).toEqual(sr.history_pair); }) ); }); it("forked ChatState results contains the SavedResponse result keyed by '1'", () => { fc.assert( fc.property(savedResponseArb, fc.uuid(), (sr, newId) => { const chatState = forkSavedResponse(sr, newId); expect(chatState.results).toEqual({ "1": sr.result }); }) ); }); it("forked ChatState has streaming state reset", () => { fc.assert( fc.property(savedResponseArb, fc.uuid(), (sr, newId) => { const chatState = forkSavedResponse(sr, newId); expect(chatState.streamingIndex).toBeNull(); expect(chatState.streamingContent).toBe(""); expect(chatState.isStreaming).toBe(false); }) ); }); it("forked ChatState conversationId is always distinct from source_conversation_id when UUIDs differ", () => { fc.assert( fc.property( savedResponseArb, fc.uuid(), (sr, newId) => { // Use fc.pre to skip cases where the generated UUID happens to match fc.pre(newId !== sr.source_conversation_id); const chatState = forkSavedResponse(sr, newId); // The new conversation ID must be the provided UUID expect(chatState.conversationId).toBe(newId); // And it must differ from the source expect(chatState.conversationId).not.toBe(sr.source_conversation_id); } ) ); }); });