import { CommandId, EnvironmentId, MessageId, ProjectId, ProviderInstanceId, ThreadId,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import { isPendingThreadCreationVisible, pendingThreadCreationMessage, pendingThreadCreationShell, resolvePendingThreadCreation, type PendingThreadCreation,} from "./pending-thread-creation";import type { QueuedThreadMessage } from "./thread-outbox-model"; const creation: QueuedThreadMessage = { environmentId: EnvironmentId.make("environment-1"), threadId: ThreadId.make("thread-1"), messageId: MessageId.make("message-1"), commandId: CommandId.make("command-1"), text: "Fix the flaky login test", attachments: [ { id: "draft-image", type: "image", name: "screen.png", mimeType: "image/png", sizeBytes: 10, previewUri: "data:image/png;base64,AAAA", }, ], modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" }, runtimeMode: "full-access", creation: { projectId: ProjectId.make("project-1"), workspaceMode: "worktree", branch: "main", worktreePath: null, }, createdAt: "2026-08-24T12:00:00.000Z",}; describe("resolvePendingThreadCreation", () => { const threadKey = `${creation.environmentId}:${creation.threadId}`; const pending: PendingThreadCreation = { message: creation, outcome: null }; const prompt = { id: creation.messageId }; it("keeps setup visible through the prompt echo and shell cleanup until detail has a turn", () => { let previous = resolvePendingThreadCreation({ threadKey, pending, previous: null, detail: null, }); expect(previous).toBe(pending); previous = resolvePendingThreadCreation({ threadKey, pending, previous, detail: { messages: [], latestTurn: null, session: null }, }); expect(previous).toBe(pending); // The user message arrives before the provider publishes a timed turn. previous = resolvePendingThreadCreation({ threadKey, pending, previous, detail: { messages: [prompt], latestTurn: null, session: { status: "starting" } }, }); expect(previous).toBe(pending); // The shell stream may observe the turn and collect the global outcome // before this screen's detail stream catches up. previous = resolvePendingThreadCreation({ threadKey, pending: null, previous, detail: { messages: [prompt], latestTurn: null, session: { status: "starting" } }, }); expect(previous).toBe(pending); expect( resolvePendingThreadCreation({ threadKey, pending: null, previous, detail: { messages: [prompt], latestTurn: { turnId: "turn-1" }, session: { status: "running" }, }, }), ).toBeNull(); }); it("keeps the prompt until both the turn and its message have arrived", () => { expect( resolvePendingThreadCreation({ threadKey, pending, previous: null, detail: { messages: [], latestTurn: { turnId: "turn-1" }, session: { status: "running" } }, }), ).toBe(pending); }); it.each(["error", "stopped", "interrupted"])("ends setup when startup is %s", (status) => { expect( resolvePendingThreadCreation({ threadKey, pending: null, previous: pending, detail: { messages: [prompt], latestTurn: null, session: { status } }, }), ).toBeNull(); }); it("preserves rejected task recovery", () => { const failed: PendingThreadCreation = { message: creation, outcome: { kind: "failed", message: creation, reason: "Checkout failed" }, }; expect( resolvePendingThreadCreation({ threadKey, pending: failed, previous: pending, detail: { messages: [], latestTurn: null, session: { status: "error" } }, }), ).toBe(failed); }); it("does not carry setup into another thread or invent it for existing threads", () => { expect( resolvePendingThreadCreation({ threadKey: "another-thread", pending: null, previous: pending, detail: null, }), ).toBeNull(); expect( resolvePendingThreadCreation({ threadKey, pending: null, previous: null, detail: null, }), ).toBeNull(); });}); describe("pendingThreadCreationShell", () => { it("shapes a queued creation as the thread shell the screen renders before creation", () => { expect(pendingThreadCreationShell(creation)).toMatchObject({ environmentId: creation.environmentId, id: creation.threadId, projectId: creation.creation!.projectId, title: "Fix the flaky login test", modelSelection: creation.modelSelection, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, latestTurn: null, session: null, latestUserMessageAt: creation.createdAt, }); }); it("keeps a local task's explicit worktree path", () => { expect( pendingThreadCreationShell({ ...creation, creation: { ...creation.creation!, workspaceMode: "local", worktreePath: "/repo/.worktrees/feature", }, })?.worktreePath, ).toBe("/repo/.worktrees/feature"); }); it("returns null for a follow-up message or a creation without a model", () => { expect(pendingThreadCreationShell({ ...creation, creation: undefined })).toBeNull(); expect(pendingThreadCreationShell({ ...creation, modelSelection: undefined })).toBeNull(); });}); describe("isPendingThreadCreationVisible", () => { const creationMessageId = String(creation.messageId); it("stands in before any detail has loaded", () => { expect(isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: null })).toBe( true, ); }); // The regression: the server creates the thread, THEN builds the worktree, // then starts the turn. The shell and an empty detail arrive seconds before // the prompt, and keying on the shell left the thread empty for that whole // window. it("keeps standing in while the created thread has no messages yet", () => { expect(isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: [] })).toBe(true); }); it("keeps standing in when the thread holds only unrelated messages", () => { expect( isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: ["someone-else"] }), ).toBe(true); }); it("stands down once the delivered prompt lands under the same id", () => { expect( isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: ["someone-else", creationMessageId], }), ).toBe(false); });}); describe("pendingThreadCreationMessage", () => { it("renders the queued prompt as the first user message", () => { expect(pendingThreadCreationMessage(creation)).toEqual({ id: creation.messageId, role: "user", text: creation.text, turnId: null, streaming: false, createdAt: creation.createdAt, updatedAt: creation.createdAt, }); }); // Draft attachment ids are local; the feed resolves attachment rows against // the server and would spin forever on them. it("omits the queued attachments rather than passing local draft ids to the feed", () => { expect(pendingThreadCreationMessage(creation)).not.toHaveProperty("attachments"); });});
import {
CommandId,
EnvironmentId,
MessageId,
ProjectId,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
import {
isPendingThreadCreationVisible,
pendingThreadCreationMessage,
pendingThreadCreationShell,
resolvePendingThreadCreation,
type PendingThreadCreation,
} from "./pending-thread-creation";
import type { QueuedThreadMessage } from "./thread-outbox-model";
const creation: QueuedThreadMessage = {
environmentId: EnvironmentId.make("environment-1"),
threadId: ThreadId.make("thread-1"),
messageId: MessageId.make("message-1"),
commandId: CommandId.make("command-1"),
text: "Fix the flaky login test",
attachments: [
{
id: "draft-image",
type: "image",
name: "screen.png",
mimeType: "image/png",
sizeBytes: 10,
previewUri: "data:image/png;base64,AAAA",
},
],
modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
runtimeMode: "full-access",
creation: {
projectId: ProjectId.make("project-1"),
workspaceMode: "worktree",
branch: "main",
worktreePath: null,
},
createdAt: "2026-08-24T12:00:00.000Z",
};
describe("resolvePendingThreadCreation", () => {
const threadKey = `${creation.environmentId}:${creation.threadId}`;
const pending: PendingThreadCreation = { message: creation, outcome: null };
const prompt = { id: creation.messageId };
it("keeps setup visible through the prompt echo and shell cleanup until detail has a turn", () => {
let previous = resolvePendingThreadCreation({
threadKey,
pending,
previous: null,
detail: null,
});
expect(previous).toBe(pending);
previous = resolvePendingThreadCreation({
threadKey,
pending,
previous,
detail: { messages: [], latestTurn: null, session: null },
});
expect(previous).toBe(pending);
// The user message arrives before the provider publishes a timed turn.
previous = resolvePendingThreadCreation({
threadKey,
pending,
previous,
detail: { messages: [prompt], latestTurn: null, session: { status: "starting" } },
});
expect(previous).toBe(pending);
// The shell stream may observe the turn and collect the global outcome
// before this screen's detail stream catches up.
previous = resolvePendingThreadCreation({
threadKey,
pending: null,
previous,
detail: { messages: [prompt], latestTurn: null, session: { status: "starting" } },
});
expect(previous).toBe(pending);
expect(
resolvePendingThreadCreation({
threadKey,
pending: null,
previous,
detail: {
messages: [prompt],
latestTurn: { turnId: "turn-1" },
session: { status: "running" },
},
}),
).toBeNull();
});
it("keeps the prompt until both the turn and its message have arrived", () => {
expect(
resolvePendingThreadCreation({
threadKey,
pending,
previous: null,
detail: { messages: [], latestTurn: { turnId: "turn-1" }, session: { status: "running" } },
}),
).toBe(pending);
});
it.each(["error", "stopped", "interrupted"])("ends setup when startup is %s", (status) => {
expect(
resolvePendingThreadCreation({
threadKey,
pending: null,
previous: pending,
detail: { messages: [prompt], latestTurn: null, session: { status } },
}),
).toBeNull();
});
it("preserves rejected task recovery", () => {
const failed: PendingThreadCreation = {
message: creation,
outcome: { kind: "failed", message: creation, reason: "Checkout failed" },
};
expect(
resolvePendingThreadCreation({
threadKey,
pending: failed,
previous: pending,
detail: { messages: [], latestTurn: null, session: { status: "error" } },
}),
).toBe(failed);
});
it("does not carry setup into another thread or invent it for existing threads", () => {
expect(
resolvePendingThreadCreation({
threadKey: "another-thread",
pending: null,
previous: pending,
detail: null,
}),
).toBeNull();
expect(
resolvePendingThreadCreation({
threadKey,
pending: null,
previous: null,
detail: null,
}),
).toBeNull();
});
});
describe("pendingThreadCreationShell", () => {
it("shapes a queued creation as the thread shell the screen renders before creation", () => {
expect(pendingThreadCreationShell(creation)).toMatchObject({
environmentId: creation.environmentId,
id: creation.threadId,
projectId: creation.creation!.projectId,
title: "Fix the flaky login test",
modelSelection: creation.modelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: "main",
worktreePath: null,
latestTurn: null,
session: null,
latestUserMessageAt: creation.createdAt,
});
});
it("keeps a local task's explicit worktree path", () => {
expect(
pendingThreadCreationShell({
...creation,
creation: {
...creation.creation!,
workspaceMode: "local",
worktreePath: "/repo/.worktrees/feature",
},
})?.worktreePath,
).toBe("/repo/.worktrees/feature");
});
it("returns null for a follow-up message or a creation without a model", () => {
expect(pendingThreadCreationShell({ ...creation, creation: undefined })).toBeNull();
expect(pendingThreadCreationShell({ ...creation, modelSelection: undefined })).toBeNull();
});
});
describe("isPendingThreadCreationVisible", () => {
const creationMessageId = String(creation.messageId);
it("stands in before any detail has loaded", () => {
expect(isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: null })).toBe(
true,
);
});
// The regression: the server creates the thread, THEN builds the worktree,
// then starts the turn. The shell and an empty detail arrive seconds before
// the prompt, and keying on the shell left the thread empty for that whole
// window.
it("keeps standing in while the created thread has no messages yet", () => {
expect(isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: [] })).toBe(true);
});
it("keeps standing in when the thread holds only unrelated messages", () => {
expect(
isPendingThreadCreationVisible({ creationMessageId, loadedMessageIds: ["someone-else"] }),
).toBe(true);
});
it("stands down once the delivered prompt lands under the same id", () => {
expect(
isPendingThreadCreationVisible({
creationMessageId,
loadedMessageIds: ["someone-else", creationMessageId],
}),
).toBe(false);
});
});
describe("pendingThreadCreationMessage", () => {
it("renders the queued prompt as the first user message", () => {
expect(pendingThreadCreationMessage(creation)).toEqual({
id: creation.messageId,
role: "user",
text: creation.text,
turnId: null,
streaming: false,
createdAt: creation.createdAt,
updatedAt: creation.createdAt,
});
});
// Draft attachment ids are local; the feed resolves attachment rows against
// the server and would spin forever on them.
it("omits the queued attachments rather than passing local draft ids to the feed", () => {
expect(pendingThreadCreationMessage(creation)).not.toHaveProperty("attachments");
});
});