import { describe, expect, it } from "vite-plus/test"; import { CheckpointRef, CommandId, EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId,} from "@t3tools/contracts";import type { OrchestrationThread } from "@t3tools/contracts"; import { applyThreadDetailEvent } from "./threadReducer.ts"; const baseEventFields = { eventId: EventId.make("event-1"), commandId: null, causationEventId: null, correlationId: null, metadata: {},} as const; const baseThread: OrchestrationThread = { id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Test Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, pullRequests: [], deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null,}; describe("applyThreadDetailEvent", () => { describe("project events", () => { it("returns unchanged for project.created", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 1, occurredAt: "2026-04-01T01:00:00.000Z", aggregateKind: "project", aggregateId: ProjectId.make("project-1"), type: "project.created", payload: { projectId: ProjectId.make("project-1"), title: "T3 Code", workspaceRoot: "/repo", repositoryIdentity: null, defaultModelSelection: null, scripts: [], createdAt: "2026-04-01T01:00:00.000Z", updatedAt: "2026-04-01T01:00:00.000Z", deletedAt: null, }, } as any); expect(result.kind).toBe("unchanged"); }); }); describe("thread.created", () => { it("creates a fresh thread", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 1, occurredAt: "2026-04-01T01:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-2"), type: "thread.created", payload: { threadId: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "New Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, createdAt: "2026-04-01T01:00:00.000Z", updatedAt: "2026-04-01T01:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.id).toBe("thread-2"); expect(result.thread.title).toBe("New Thread"); expect(result.thread.branch).toBe("main"); expect(result.thread.messages).toEqual([]); expect(result.thread.session).toBeNull(); } }); }); describe("thread.deleted", () => { it("returns deleted signal", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 2, occurredAt: "2026-04-01T02:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.deleted", payload: { threadId: ThreadId.make("thread-1"), deletedAt: "2026-04-01T02:00:00.000Z", }, }); expect(result.kind).toBe("deleted"); }); }); describe("thread.archived / thread.unarchived", () => { it("sets archivedAt and clears title regeneration", () => { const regeneratingThread: OrchestrationThread = { ...baseThread, titleRegeneration: { requestId: CommandId.make("regenerate-title"), startedAt: "2026-04-01T02:00:00.000Z", }, }; const result = applyThreadDetailEvent(regeneratingThread, { ...baseEventFields, sequence: 3, occurredAt: "2026-04-01T03:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.archived", payload: { threadId: ThreadId.make("thread-1"), archivedAt: "2026-04-01T03:00:00.000Z", updatedAt: "2026-04-01T03:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.archivedAt).toBe("2026-04-01T03:00:00.000Z"); expect(result.thread.titleRegeneration).toBeNull(); } }); it("clears archivedAt", () => { const archivedThread = { ...baseThread, archivedAt: "2026-04-01T03:00:00.000Z" }; const result = applyThreadDetailEvent(archivedThread, { ...baseEventFields, sequence: 4, occurredAt: "2026-04-01T04:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unarchived", payload: { threadId: ThreadId.make("thread-1"), updatedAt: "2026-04-01T04:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.archivedAt).toBeNull(); } }); }); describe("thread.settled / thread.unsettled", () => { it("sets the settled override and timestamp", () => { const settledAt = "2026-04-01T05:00:00.000Z"; const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: settledAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.settled", payload: { threadId: ThreadId.make("thread-1"), settledAt, updatedAt: settledAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.settledOverride).toBe("settled"); expect(result.thread.settledAt).toBe(settledAt); expect(result.thread.activeOrderKey).toBeNull(); } }); it.each([ ["user", "active"], ["activity", null], ] as const)("unsettles for %s with override %s", (reason, settledOverride) => { const settledThread: OrchestrationThread = { ...baseThread, settledOverride: "settled", settledAt: "2026-04-01T05:00:00.000Z", }; const updatedAt = "2026-04-01T06:00:00.000Z"; const result = applyThreadDetailEvent(settledThread, { ...baseEventFields, sequence: 6, occurredAt: updatedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unsettled", payload: { threadId: ThreadId.make("thread-1"), reason, updatedAt, }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.settledOverride).toBe(settledOverride); expect(result.thread.settledAt).toBeNull(); } }); }); describe("thread.pinned / thread.unpinned", () => { it("sets pinnedAt", () => { const pinnedAt = "2026-04-01T05:00:00.000Z"; const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: pinnedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pinned", payload: { threadId: ThreadId.make("thread-1"), pinnedAt, updatedAt: pinnedAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.pinnedAt).toBe(pinnedAt); expect(result.thread.activeOrderKey).toBe("m"); } }); it("clears pinnedAt", () => { const pinnedThread: OrchestrationThread = { ...baseThread, pinnedAt: "2026-04-01T05:00:00.000Z", }; const updatedAt = "2026-04-01T06:00:00.000Z"; const result = applyThreadDetailEvent(pinnedThread, { ...baseEventFields, sequence: 6, occurredAt: updatedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unpinned", payload: { threadId: ThreadId.make("thread-1"), updatedAt, }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.pinnedAt).toBeNull(); } }); }); describe("thread.meta-updated", () => { it.each(["f", null] as const)( "updates the active key to %s without activity", (activeOrderKey) => { const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: baseThread.id, type: "thread.meta-updated", payload: { threadId: baseThread.id, activeOrderKey, updatedAt: baseThread.updatedAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.activeOrderKey).toBe(activeOrderKey); expect(result.thread.updatedAt).toBe(baseThread.updatedAt); } }, ); it("patches title and branch", () => { const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), title: "Updated Title", branch: "feature/demo", updatedAt: "2026-04-01T05:00:00.000Z", }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.title).toBe("Updated Title"); expect(result.thread.branch).toBe("feature/demo"); expect(result.thread.activeOrderKey).toBe("m"); // Model selection should be unchanged since it wasn't in the payload expect(result.thread.modelSelection).toEqual(baseThread.modelSelection); } }); it.each(["linkedPullRequest", "branchPullRequest"] as const)( "sets and clears %s without changing the other link", (field) => { const linkedPullRequest = { projectId: ProjectId.make("project-1"), repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", }; const otherField = field === "linkedPullRequest" ? "branchPullRequest" : "linkedPullRequest"; const otherPullRequest = { ...linkedPullRequest, number: 43, url: "https://github.com/pingdotgg/t3code/pull/43", }; const linked = applyThreadDetailEvent( { ...baseThread, [otherField]: otherPullRequest }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), [field]: linkedPullRequest, updatedAt: "2026-04-01T05:00:00.000Z", }, }, ); expect(linked.kind).toBe("updated"); if (linked.kind !== "updated") return; expect(linked.thread[field]).toEqual(linkedPullRequest); expect(linked.thread[otherField]).toEqual(otherPullRequest); const cleared = applyThreadDetailEvent(linked.thread, { ...baseEventFields, sequence: 6, occurredAt: "2026-04-01T06:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), [field]: null, updatedAt: "2026-04-01T06:00:00.000Z", }, }); expect(cleared.kind).toBe("updated"); if (cleared.kind === "updated") { expect(cleared.thread[field]).toBeNull(); expect(cleared.thread[otherField]).toEqual(otherPullRequest); } }, ); }); describe("thread pull request links", () => { const link = { host: "github.com", repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", source: "manual" as const, linkedAt: "2026-04-01T05:00:00.000Z", snapshot: null, stack: null, }; const key = { host: "github.com", repository: "pingdotgg/t3code", number: 42 }; const linkEvent = (sequence: number) => ({ ...baseEventFields, sequence, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-linked", payload: { threadId: ThreadId.make("thread-1"), link, updatedAt: "2026-04-01T05:00:00.000Z", }, }) as const; it("links without inventing a legacy route and replaces by key", () => { const linked = applyThreadDetailEvent(baseThread, linkEvent(5)); expect(linked.kind).toBe("updated"); if (linked.kind !== "updated") return; expect(linked.thread.pullRequests).toEqual([link]); expect(linked.thread.linkedPullRequest).toBeNull(); const relinked = applyThreadDetailEvent(linked.thread, { ...linkEvent(6), payload: { threadId: ThreadId.make("thread-1"), link: { ...link, host: "GitHub.com", source: "agent" }, updatedAt: "2026-04-01T06:00:00.000Z", }, }); if (relinked.kind !== "updated") throw new Error("expected update"); expect(relinked.thread.pullRequests).toHaveLength(1); expect(relinked.thread.pullRequests[0]?.source).toBe("agent"); }); it("syncs snapshot and stack onto the matching link only", () => { const linked = applyThreadDetailEvent(baseThread, linkEvent(5)); if (linked.kind !== "updated") throw new Error("expected update"); const snapshot = { state: "merged" as const, title: "Ship it", headBranch: "feature", baseBranch: "main", isDraft: false, updatedAt: "2026-04-02T00:00:00.000Z", syncedAt: "2026-04-02T00:01:00.000Z", }; const synced = applyThreadDetailEvent(linked.thread, { ...baseEventFields, sequence: 6, occurredAt: "2026-04-02T00:01:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-synced", payload: { threadId: ThreadId.make("thread-1"), ...key, snapshot, stack: null, updatedAt: "2026-04-02T00:01:00.000Z", }, }); if (synced.kind !== "updated") throw new Error("expected update"); expect(synced.thread.pullRequests[0]?.snapshot).toEqual(snapshot); const unknown = applyThreadDetailEvent(synced.thread, { ...baseEventFields, sequence: 7, occurredAt: "2026-04-02T00:02:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-synced", payload: { threadId: ThreadId.make("thread-1"), ...key, number: 99, snapshot, stack: null, updatedAt: "2026-04-02T00:02:00.000Z",Lines 1–500 of 1580. Load more
import { describe, expect, it } from "vite-plus/test"; import { CheckpointRef, CommandId, EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, } from "@t3tools/contracts"; import type { OrchestrationThread } from "@t3tools/contracts"; import { applyThreadDetailEvent } from "./threadReducer.ts"; const baseEventFields = { eventId: EventId.make("event-1"), commandId: null, causationEventId: null, correlationId: null, metadata: {}, } as const; const baseThread: OrchestrationThread = { id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Test Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, pullRequests: [], deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, }; describe("applyThreadDetailEvent", () => { describe("project events", () => { it("returns unchanged for project.created", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 1, occurredAt: "2026-04-01T01:00:00.000Z", aggregateKind: "project", aggregateId: ProjectId.make("project-1"), type: "project.created", payload: { projectId: ProjectId.make("project-1"), title: "T3 Code", workspaceRoot: "/repo", repositoryIdentity: null, defaultModelSelection: null, scripts: [], createdAt: "2026-04-01T01:00:00.000Z", updatedAt: "2026-04-01T01:00:00.000Z", deletedAt: null, }, } as any); expect(result.kind).toBe("unchanged"); }); }); describe("thread.created", () => { it("creates a fresh thread", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 1, occurredAt: "2026-04-01T01:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-2"), type: "thread.created", payload: { threadId: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "New Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: "main", worktreePath: null, createdAt: "2026-04-01T01:00:00.000Z", updatedAt: "2026-04-01T01:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.id).toBe("thread-2"); expect(result.thread.title).toBe("New Thread"); expect(result.thread.branch).toBe("main"); expect(result.thread.messages).toEqual([]); expect(result.thread.session).toBeNull(); } }); }); describe("thread.deleted", () => { it("returns deleted signal", () => { const result = applyThreadDetailEvent(baseThread, { ...baseEventFields, sequence: 2, occurredAt: "2026-04-01T02:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.deleted", payload: { threadId: ThreadId.make("thread-1"), deletedAt: "2026-04-01T02:00:00.000Z", }, }); expect(result.kind).toBe("deleted"); }); }); describe("thread.archived / thread.unarchived", () => { it("sets archivedAt and clears title regeneration", () => { const regeneratingThread: OrchestrationThread = { ...baseThread, titleRegeneration: { requestId: CommandId.make("regenerate-title"), startedAt: "2026-04-01T02:00:00.000Z", }, }; const result = applyThreadDetailEvent(regeneratingThread, { ...baseEventFields, sequence: 3, occurredAt: "2026-04-01T03:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.archived", payload: { threadId: ThreadId.make("thread-1"), archivedAt: "2026-04-01T03:00:00.000Z", updatedAt: "2026-04-01T03:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.archivedAt).toBe("2026-04-01T03:00:00.000Z"); expect(result.thread.titleRegeneration).toBeNull(); } }); it("clears archivedAt", () => { const archivedThread = { ...baseThread, archivedAt: "2026-04-01T03:00:00.000Z" }; const result = applyThreadDetailEvent(archivedThread, { ...baseEventFields, sequence: 4, occurredAt: "2026-04-01T04:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unarchived", payload: { threadId: ThreadId.make("thread-1"), updatedAt: "2026-04-01T04:00:00.000Z", }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.archivedAt).toBeNull(); } }); }); describe("thread.settled / thread.unsettled", () => { it("sets the settled override and timestamp", () => { const settledAt = "2026-04-01T05:00:00.000Z"; const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: settledAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.settled", payload: { threadId: ThreadId.make("thread-1"), settledAt, updatedAt: settledAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.settledOverride).toBe("settled"); expect(result.thread.settledAt).toBe(settledAt); expect(result.thread.activeOrderKey).toBeNull(); } }); it.each([ ["user", "active"], ["activity", null], ] as const)("unsettles for %s with override %s", (reason, settledOverride) => { const settledThread: OrchestrationThread = { ...baseThread, settledOverride: "settled", settledAt: "2026-04-01T05:00:00.000Z", }; const updatedAt = "2026-04-01T06:00:00.000Z"; const result = applyThreadDetailEvent(settledThread, { ...baseEventFields, sequence: 6, occurredAt: updatedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unsettled", payload: { threadId: ThreadId.make("thread-1"), reason, updatedAt, }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.settledOverride).toBe(settledOverride); expect(result.thread.settledAt).toBeNull(); } }); }); describe("thread.pinned / thread.unpinned", () => { it("sets pinnedAt", () => { const pinnedAt = "2026-04-01T05:00:00.000Z"; const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: pinnedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pinned", payload: { threadId: ThreadId.make("thread-1"), pinnedAt, updatedAt: pinnedAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.pinnedAt).toBe(pinnedAt); expect(result.thread.activeOrderKey).toBe("m"); } }); it("clears pinnedAt", () => { const pinnedThread: OrchestrationThread = { ...baseThread, pinnedAt: "2026-04-01T05:00:00.000Z", }; const updatedAt = "2026-04-01T06:00:00.000Z"; const result = applyThreadDetailEvent(pinnedThread, { ...baseEventFields, sequence: 6, occurredAt: updatedAt, aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.unpinned", payload: { threadId: ThreadId.make("thread-1"), updatedAt, }, }); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.pinnedAt).toBeNull(); } }); }); describe("thread.meta-updated", () => { it.each(["f", null] as const)( "updates the active key to %s without activity", (activeOrderKey) => { const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: baseThread.id, type: "thread.meta-updated", payload: { threadId: baseThread.id, activeOrderKey, updatedAt: baseThread.updatedAt, }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.activeOrderKey).toBe(activeOrderKey); expect(result.thread.updatedAt).toBe(baseThread.updatedAt); } }, ); it("patches title and branch", () => { const result = applyThreadDetailEvent( { ...baseThread, activeOrderKey: "m" }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), title: "Updated Title", branch: "feature/demo", updatedAt: "2026-04-01T05:00:00.000Z", }, }, ); expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.title).toBe("Updated Title"); expect(result.thread.branch).toBe("feature/demo"); expect(result.thread.activeOrderKey).toBe("m"); // Model selection should be unchanged since it wasn't in the payload expect(result.thread.modelSelection).toEqual(baseThread.modelSelection); } }); it.each(["linkedPullRequest", "branchPullRequest"] as const)( "sets and clears %s without changing the other link", (field) => { const linkedPullRequest = { projectId: ProjectId.make("project-1"), repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", }; const otherField = field === "linkedPullRequest" ? "branchPullRequest" : "linkedPullRequest"; const otherPullRequest = { ...linkedPullRequest, number: 43, url: "https://github.com/pingdotgg/t3code/pull/43", }; const linked = applyThreadDetailEvent( { ...baseThread, [otherField]: otherPullRequest }, { ...baseEventFields, sequence: 5, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), [field]: linkedPullRequest, updatedAt: "2026-04-01T05:00:00.000Z", }, }, ); expect(linked.kind).toBe("updated"); if (linked.kind !== "updated") return; expect(linked.thread[field]).toEqual(linkedPullRequest); expect(linked.thread[otherField]).toEqual(otherPullRequest); const cleared = applyThreadDetailEvent(linked.thread, { ...baseEventFields, sequence: 6, occurredAt: "2026-04-01T06:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.meta-updated", payload: { threadId: ThreadId.make("thread-1"), [field]: null, updatedAt: "2026-04-01T06:00:00.000Z", }, }); expect(cleared.kind).toBe("updated"); if (cleared.kind === "updated") { expect(cleared.thread[field]).toBeNull(); expect(cleared.thread[otherField]).toEqual(otherPullRequest); } }, ); }); describe("thread pull request links", () => { const link = { host: "github.com", repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", source: "manual" as const, linkedAt: "2026-04-01T05:00:00.000Z", snapshot: null, stack: null, }; const key = { host: "github.com", repository: "pingdotgg/t3code", number: 42 }; const linkEvent = (sequence: number) => ({ ...baseEventFields, sequence, occurredAt: "2026-04-01T05:00:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-linked", payload: { threadId: ThreadId.make("thread-1"), link, updatedAt: "2026-04-01T05:00:00.000Z", }, }) as const; it("links without inventing a legacy route and replaces by key", () => { const linked = applyThreadDetailEvent(baseThread, linkEvent(5)); expect(linked.kind).toBe("updated"); if (linked.kind !== "updated") return; expect(linked.thread.pullRequests).toEqual([link]); expect(linked.thread.linkedPullRequest).toBeNull(); const relinked = applyThreadDetailEvent(linked.thread, { ...linkEvent(6), payload: { threadId: ThreadId.make("thread-1"), link: { ...link, host: "GitHub.com", source: "agent" }, updatedAt: "2026-04-01T06:00:00.000Z", }, }); if (relinked.kind !== "updated") throw new Error("expected update"); expect(relinked.thread.pullRequests).toHaveLength(1); expect(relinked.thread.pullRequests[0]?.source).toBe("agent"); }); it("syncs snapshot and stack onto the matching link only", () => { const linked = applyThreadDetailEvent(baseThread, linkEvent(5)); if (linked.kind !== "updated") throw new Error("expected update"); const snapshot = { state: "merged" as const, title: "Ship it", headBranch: "feature", baseBranch: "main", isDraft: false, updatedAt: "2026-04-02T00:00:00.000Z", syncedAt: "2026-04-02T00:01:00.000Z", }; const synced = applyThreadDetailEvent(linked.thread, { ...baseEventFields, sequence: 6, occurredAt: "2026-04-02T00:01:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-synced", payload: { threadId: ThreadId.make("thread-1"), ...key, snapshot, stack: null, updatedAt: "2026-04-02T00:01:00.000Z", }, }); if (synced.kind !== "updated") throw new Error("expected update"); expect(synced.thread.pullRequests[0]?.snapshot).toEqual(snapshot); const unknown = applyThreadDetailEvent(synced.thread, { ...baseEventFields, sequence: 7, occurredAt: "2026-04-02T00:02:00.000Z", aggregateKind: "thread", aggregateId: ThreadId.make("thread-1"), type: "thread.pull-request-synced", payload: { threadId: ThreadId.make("thread-1"), ...key, number: 99, snapshot, stack: null, updatedAt: "2026-04-02T00:02:00.000Z",