Spaces:
Runtime error
Runtime error
| /** | |
| * 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<Message> = fc.record({ | |
| role: fc.constantFrom("user" as const, "assistant" as const), | |
| content: fc.string({ minLength: 0, maxLength: 500 }), | |
| }); | |
| /** Arbitrary QueryResult */ | |
| const queryResultArb: fc.Arbitrary<QueryResult> = 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<SavedResponse> = 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); | |
| } | |
| ) | |
| ); | |
| }); | |
| }); | |