hmc-rag / frontend /src /lib /fork.test.ts
webmuppet
Refactor: replace Streamlit with React+Vite SPA + FastAPI server
b5c0df4
Raw
History Blame Contribute Delete
6.06 kB
/**
* 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);
}
)
);
});
});