import { ASSISTANT_CITATION_MAX_TEXT_LENGTH, EnvironmentId, MessageId, PROVIDER_SEND_TURN_MAX_INPUT_CHARS, ThreadId,} from "@t3tools/contracts";import { expandAssistantCitationsForProvider, serializeAssistantCitation,} from "@t3tools/shared/assistantCitations";import { describe, expect, it, vi } from "vite-plus/test"; import { submitComposerDraft } from "./composerSubmission"; const assistantCitation = { version: 1 as const, environmentId: EnvironmentId.make("source-environment"), threadId: ThreadId.make("source-thread"), messageId: MessageId.make("source-message"), text: "Keep the parser shared.", start: 0, end: 23, prefix: "", suffix: "",}; describe("submitComposerDraft", () => { it("keeps an oversized draft editable and sends a corrected follow-up", () => { let draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1); let validationMessage: string | null = null; const dispatchedDrafts: string[] = []; const preventDefault = vi.fn(); const submit = () => { const result = submitComposerDraft({ prompt: draft, submissionTarget: "provider-turn", event: { preventDefault }, onSend: () => { dispatchedDrafts.push(draft); }, }); validationMessage = result.validationMessage; }; submit(); expect(dispatchedDrafts).toEqual([]); expect(draft).toHaveLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1); expect(validationMessage).toBe( "Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.", ); expect(preventDefault).toHaveBeenCalledOnce(); draft = "Corrected prompt"; submit(); expect(dispatchedDrafts).toEqual(["Corrected prompt"]); expect(validationMessage).toBeNull(); }); it("allows a draft at the shared character limit through the normal send path", () => { const draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS); const onSend = vi.fn(); const preventDefault = vi.fn(); const result = submitComposerDraft({ prompt: draft, submissionTarget: "provider-turn", event: { preventDefault }, onSend, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); expect(preventDefault).not.toHaveBeenCalled(); }); it("blocks when appended context pushes the provider input over the shared limit", () => { const draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS); const onSend = vi.fn(); const result = submitComposerDraft({ prompt: draft, providerInput: `${draft}\n\nTerminal context`, submissionTarget: "provider-turn", event: undefined, onSend, }); expect(result).toEqual({ validationMessage: "Prompt is 18 characters over the 120,000-character limit. Shorten or split it before sending.", didDispatch: false, }); expect(draft).toHaveLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS); expect(onSend).not.toHaveBeenCalled(); const correctedResult = submitComposerDraft({ prompt: "Corrected prompt", providerInput: "Corrected prompt\n\nShort terminal context", submissionTarget: "provider-turn", event: undefined, onSend, }); expect(correctedResult).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); }); it("does not finish submission when the send boundary rejects composed provider input", () => { const preventDefault = vi.fn(); const result = submitComposerDraft({ prompt: "Sendable raw draft", submissionTarget: "provider-turn", event: { preventDefault }, onSend: () => false, }); expect(result).toEqual({ validationMessage: null, didDispatch: false }); expect(preventDefault).toHaveBeenCalledOnce(); }); it("allows fully composed provider input at the shared character limit", () => { const onSend = vi.fn(); const result = submitComposerDraft({ prompt: "Short draft", providerInput: "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS), submissionTarget: "provider-turn", event: undefined, onSend, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); }); it.each(["draft", "composed provider input"])( "blocks a %s when citation expansion exceeds the shared limit", (source) => { const citation = serializeAssistantCitation(assistantCitation); const followUp = `Explain ${citation}\n\nTerminal context`; const providerInput = `${"x".repeat( PROVIDER_SEND_TURN_MAX_INPUT_CHARS - expandAssistantCitationsForProvider(followUp).length + 1, )}${followUp}`; const onSend = vi.fn(); const preventDefault = vi.fn(); expect(providerInput.length).toBeLessThan(PROVIDER_SEND_TURN_MAX_INPUT_CHARS); const result = submitComposerDraft({ prompt: source === "draft" ? providerInput : "Explain the quote", ...(source === "composed provider input" ? { providerInput } : {}), submissionTarget: "provider-turn", event: { preventDefault }, onSend, }); expect(result).toEqual({ validationMessage: "Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.", didDispatch: false, }); expect(onSend).not.toHaveBeenCalled(); expect(preventDefault).toHaveBeenCalledOnce(); }, ); it("keeps the encoded-message limit when citation expansion is shorter", () => { const citation = serializeAssistantCitation({ ...assistantCitation, text: "é".repeat(ASSISTANT_CITATION_MAX_TEXT_LENGTH), end: ASSISTANT_CITATION_MAX_TEXT_LENGTH, }); const draft = `${"x".repeat( PROVIDER_SEND_TURN_MAX_INPUT_CHARS - citation.length + 1, )}${citation}`; const onSend = vi.fn(); expect(expandAssistantCitationsForProvider(draft).length).toBeLessThan( PROVIDER_SEND_TURN_MAX_INPUT_CHARS, ); const result = submitComposerDraft({ prompt: draft, submissionTarget: "provider-turn", event: undefined, onSend, }); expect(result).toEqual({ validationMessage: "Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.", didDispatch: false, }); expect(onSend).not.toHaveBeenCalled(); }); it("sends canonical citations when expanded input fits exactly after trimming", () => { const citation = serializeAssistantCitation(assistantCitation); const draft = ` ${"x".repeat( PROVIDER_SEND_TURN_MAX_INPUT_CHARS - expandAssistantCitationsForProvider(citation).length, )}${citation} \n`; const dispatchedDrafts: string[] = []; const result = submitComposerDraft({ prompt: draft, submissionTarget: "provider-turn", event: undefined, onSend: () => { dispatchedDrafts.push(draft); }, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(dispatchedDrafts).toEqual([draft]); expect(dispatchedDrafts[0]).toContain(citation); expect(dispatchedDrafts[0]).not.toContain("<assistant_citations>"); }); it("blocks a generated plan follow-up that exceeds the shared limit", () => { const onSend = vi.fn(); const result = submitComposerDraft({ prompt: "", providerInput: `PLEASE IMPLEMENT THIS PLAN:\n${"x".repeat( PROVIDER_SEND_TURN_MAX_INPUT_CHARS, )}`, submissionTarget: "provider-turn", event: undefined, onSend, }); expect(result.didDispatch).toBe(false); expect(result.validationMessage).toContain("over the 120,000-character limit"); expect(onSend).not.toHaveBeenCalled(); }); it("allows surrounding whitespace that the provider turn contract trims", () => { const draft = ` ${"x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)} `; const onSend = vi.fn(); const preventDefault = vi.fn(); const result = submitComposerDraft({ prompt: draft, submissionTarget: "provider-turn", event: { preventDefault }, onSend, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); expect(preventDefault).not.toHaveBeenCalled(); }); it("dispatches pending user input answers on their separate response path", () => { const answer = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1); const onSend = vi.fn(); const preventDefault = vi.fn(); const result = submitComposerDraft({ prompt: answer, submissionTarget: "pending-user-input", event: { preventDefault }, onSend, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); expect(preventDefault).not.toHaveBeenCalled(); }); it("does not apply citation-expanded prompt limits to pending user input answers", () => { const citation = serializeAssistantCitation(assistantCitation); const answer = `${"x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS - citation.length)}${citation}`; const onSend = vi.fn(); expect(expandAssistantCitationsForProvider(answer).length).toBeGreaterThan( PROVIDER_SEND_TURN_MAX_INPUT_CHARS, ); const result = submitComposerDraft({ prompt: answer, submissionTarget: "pending-user-input", event: undefined, onSend, }); expect(result).toEqual({ validationMessage: null, didDispatch: true }); expect(onSend).toHaveBeenCalledOnce(); });});
import {
ASSISTANT_CITATION_MAX_TEXT_LENGTH,
EnvironmentId,
MessageId,
PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
ThreadId,
} from "@t3tools/contracts";
import {
expandAssistantCitationsForProvider,
serializeAssistantCitation,
} from "@t3tools/shared/assistantCitations";
import { describe, expect, it, vi } from "vite-plus/test";
import { submitComposerDraft } from "./composerSubmission";
const assistantCitation = {
version: 1 as const,
environmentId: EnvironmentId.make("source-environment"),
threadId: ThreadId.make("source-thread"),
messageId: MessageId.make("source-message"),
text: "Keep the parser shared.",
start: 0,
end: 23,
prefix: "",
suffix: "",
};
describe("submitComposerDraft", () => {
it("keeps an oversized draft editable and sends a corrected follow-up", () => {
let draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1);
let validationMessage: string | null = null;
const dispatchedDrafts: string[] = [];
const preventDefault = vi.fn();
const submit = () => {
const result = submitComposerDraft({
prompt: draft,
submissionTarget: "provider-turn",
event: { preventDefault },
onSend: () => {
dispatchedDrafts.push(draft);
},
});
validationMessage = result.validationMessage;
};
submit();
expect(dispatchedDrafts).toEqual([]);
expect(draft).toHaveLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1);
expect(validationMessage).toBe(
"Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.",
);
expect(preventDefault).toHaveBeenCalledOnce();
draft = "Corrected prompt";
submit();
expect(dispatchedDrafts).toEqual(["Corrected prompt"]);
expect(validationMessage).toBeNull();
});
it("allows a draft at the shared character limit through the normal send path", () => {
const draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
const onSend = vi.fn();
const preventDefault = vi.fn();
const result = submitComposerDraft({
prompt: draft,
submissionTarget: "provider-turn",
event: { preventDefault },
onSend,
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
expect(preventDefault).not.toHaveBeenCalled();
});
it("blocks when appended context pushes the provider input over the shared limit", () => {
const draft = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
const onSend = vi.fn();
const result = submitComposerDraft({
prompt: draft,
providerInput: `${draft}\n\nTerminal context`,
submissionTarget: "provider-turn",
event: undefined,
onSend,
});
expect(result).toEqual({
validationMessage:
"Prompt is 18 characters over the 120,000-character limit. Shorten or split it before sending.",
didDispatch: false,
});
expect(draft).toHaveLength(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
expect(onSend).not.toHaveBeenCalled();
const correctedResult = submitComposerDraft({
prompt: "Corrected prompt",
providerInput: "Corrected prompt\n\nShort terminal context",
submissionTarget: "provider-turn",
event: undefined,
onSend,
});
expect(correctedResult).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
});
it("does not finish submission when the send boundary rejects composed provider input", () => {
const preventDefault = vi.fn();
const result = submitComposerDraft({
prompt: "Sendable raw draft",
submissionTarget: "provider-turn",
event: { preventDefault },
onSend: () => false,
});
expect(result).toEqual({ validationMessage: null, didDispatch: false });
expect(preventDefault).toHaveBeenCalledOnce();
});
it("allows fully composed provider input at the shared character limit", () => {
const onSend = vi.fn();
const result = submitComposerDraft({
prompt: "Short draft",
providerInput: "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS),
submissionTarget: "provider-turn",
event: undefined,
onSend,
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
});
it.each(["draft", "composed provider input"])(
"blocks a %s when citation expansion exceeds the shared limit",
(source) => {
const citation = serializeAssistantCitation(assistantCitation);
const followUp = `Explain ${citation}\n\nTerminal context`;
const providerInput = `${"x".repeat(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS -
expandAssistantCitationsForProvider(followUp).length +
1,
)}${followUp}`;
const onSend = vi.fn();
const preventDefault = vi.fn();
expect(providerInput.length).toBeLessThan(PROVIDER_SEND_TURN_MAX_INPUT_CHARS);
const result = submitComposerDraft({
prompt: source === "draft" ? providerInput : "Explain the quote",
...(source === "composed provider input" ? { providerInput } : {}),
submissionTarget: "provider-turn",
event: { preventDefault },
onSend,
});
expect(result).toEqual({
validationMessage:
"Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.",
didDispatch: false,
});
expect(onSend).not.toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalledOnce();
},
);
it("keeps the encoded-message limit when citation expansion is shorter", () => {
const citation = serializeAssistantCitation({
...assistantCitation,
text: "é".repeat(ASSISTANT_CITATION_MAX_TEXT_LENGTH),
end: ASSISTANT_CITATION_MAX_TEXT_LENGTH,
});
const draft = `${"x".repeat(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS - citation.length + 1,
)}${citation}`;
const onSend = vi.fn();
expect(expandAssistantCitationsForProvider(draft).length).toBeLessThan(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
);
const result = submitComposerDraft({
prompt: draft,
submissionTarget: "provider-turn",
event: undefined,
onSend,
});
expect(result).toEqual({
validationMessage:
"Prompt is 1 character over the 120,000-character limit. Shorten or split it before sending.",
didDispatch: false,
});
expect(onSend).not.toHaveBeenCalled();
});
it("sends canonical citations when expanded input fits exactly after trimming", () => {
const citation = serializeAssistantCitation(assistantCitation);
const draft = ` ${"x".repeat(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS - expandAssistantCitationsForProvider(citation).length,
)}${citation} \n`;
const dispatchedDrafts: string[] = [];
const result = submitComposerDraft({
prompt: draft,
submissionTarget: "provider-turn",
event: undefined,
onSend: () => {
dispatchedDrafts.push(draft);
},
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(dispatchedDrafts).toEqual([draft]);
expect(dispatchedDrafts[0]).toContain(citation);
expect(dispatchedDrafts[0]).not.toContain("<assistant_citations>");
});
it("blocks a generated plan follow-up that exceeds the shared limit", () => {
const onSend = vi.fn();
const result = submitComposerDraft({
prompt: "",
providerInput: `PLEASE IMPLEMENT THIS PLAN:\n${"x".repeat(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
)}`,
submissionTarget: "provider-turn",
event: undefined,
onSend,
});
expect(result.didDispatch).toBe(false);
expect(result.validationMessage).toContain("over the 120,000-character limit");
expect(onSend).not.toHaveBeenCalled();
});
it("allows surrounding whitespace that the provider turn contract trims", () => {
const draft = ` ${"x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)} `;
const onSend = vi.fn();
const preventDefault = vi.fn();
const result = submitComposerDraft({
prompt: draft,
submissionTarget: "provider-turn",
event: { preventDefault },
onSend,
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
expect(preventDefault).not.toHaveBeenCalled();
});
it("dispatches pending user input answers on their separate response path", () => {
const answer = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS + 1);
const onSend = vi.fn();
const preventDefault = vi.fn();
const result = submitComposerDraft({
prompt: answer,
submissionTarget: "pending-user-input",
event: { preventDefault },
onSend,
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
expect(preventDefault).not.toHaveBeenCalled();
});
it("does not apply citation-expanded prompt limits to pending user input answers", () => {
const citation = serializeAssistantCitation(assistantCitation);
const answer = `${"x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS - citation.length)}${citation}`;
const onSend = vi.fn();
expect(expandAssistantCitationsForProvider(answer).length).toBeGreaterThan(
PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
);
const result = submitComposerDraft({
prompt: answer,
submissionTarget: "pending-user-input",
event: undefined,
onSend,
});
expect(result).toEqual({ validationMessage: null, didDispatch: true });
expect(onSend).toHaveBeenCalledOnce();
});
});