247 lines · 7.4 KB · TypeScript
RawDownload
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");  });});