import { ProjectId, ThreadId } from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import { v2Project, v2ShellSnapshot, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";import { applyShellStreamEvent, mergeShellSnapshotProjects } from "./shellReducer.ts"; const repositoryIdentity = { canonicalKey: "github.com/example/repo", locator: { source: "git-remote" as const, remoteName: "origin", remoteUrl: "https://github.com/example/repo.git", },}; const otherIdentity = { canonicalKey: "github.com/example/other", locator: { source: "git-remote" as const, remoteName: "origin", remoteUrl: "https://github.com/example/other.git", },}; describe("applyShellStreamEvent", () => { it("ignores stale project updates without mutating the snapshot", () => { const snapshotWithProject = { ...v2ShellSnapshot, snapshotSequence: 4, projects: [v2Project], }; for (const sequence of [3, 4]) { const next = applyShellStreamEvent(snapshotWithProject, { kind: "project.updated", sequence, project: { ...v2Project, title: "Stale Title" }, }); expect(next).toBe(snapshotWithProject); expect(next.snapshotSequence).toBe(4); expect(next.projects[0]?.title).toBe(v2Project.title); } }); it("applies project updates and removals", () => { const updated = applyShellStreamEvent(v2ShellSnapshot, { kind: "project.updated", sequence: 1, project: { ...v2Project, title: "Updated" }, }); expect(updated.projects[0]?.title).toBe("Updated"); expect(updated.snapshotSequence).toBe(1); const removed = applyShellStreamEvent(updated, { kind: "project.removed", sequence: 2, projectId: ProjectId.make(v2Project.id), }); expect(removed.projects).toEqual([]); }); it("keeps prior repositoryIdentity when a delta arrives with null identity", () => { const withIdentity = applyShellStreamEvent(v2ShellSnapshot, { kind: "project.updated", sequence: 1, project: { ...v2Project, repositoryIdentity }, }); expect(withIdentity.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); const withoutIdentity = applyShellStreamEvent(withIdentity, { kind: "project.updated", sequence: 2, project: { ...v2Project, title: "Still same root", repositoryIdentity: null }, }); expect(withoutIdentity.projects[0]?.title).toBe("Still same root"); expect(withoutIdentity.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); }); it("does not keep prior repositoryIdentity after a workspace root move", () => { const withIdentity = applyShellStreamEvent(v2ShellSnapshot, { kind: "project.updated", sequence: 1, project: { ...v2Project, repositoryIdentity }, }); const moved = applyShellStreamEvent(withIdentity, { kind: "project.updated", sequence: 2, project: { ...v2Project, workspaceRoot: "/tmp/other-root", repositoryIdentity: null, }, }); expect(moved.projects[0]?.workspaceRoot).toBe("/tmp/other-root"); expect(moved.projects[0]?.repositoryIdentity).toBeNull(); }); it("merges full snapshot projects without dropping known repositoryIdentity", () => { const previous = { ...v2ShellSnapshot, projects: [{ ...v2Project, repositoryIdentity }], }; const next = mergeShellSnapshotProjects(previous, { ...v2ShellSnapshot, snapshotSequence: 2, projects: [{ ...v2Project, title: "Refreshed", repositoryIdentity: null }], }); expect(next.projects[0]?.title).toBe("Refreshed"); expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); }); it("authoritative lower-sequence reset replaces client-ahead structural state", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 9, projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }], threads: [{ ...v2ThreadShell, title: "Local-only thread" }], }; const next = mergeShellSnapshotProjects(previous, { ...v2ShellSnapshot, snapshotSequence: 3, projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }], threads: [{ ...v2ThreadShell, title: "Server thread" }], }); expect(next.snapshotSequence).toBe(3); expect(next.projects[0]?.title).toBe("Server reset"); expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); expect(next.threads[0]?.title).toBe("Server thread"); }); it("client-ahead survives lower marked enrichment then still resets on unmarked authoritative", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 20, projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }], threads: [{ ...v2ThreadShell, title: "Local-only thread" }], }; const serverSnapshot = { ...v2ShellSnapshot, snapshotSequence: 3, projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }], threads: [{ ...v2ThreadShell, title: "Server thread" }], }; // Lower-sequence marked enrichment is identity-only and must not replace structure. const afterEnrichment = mergeShellSnapshotProjects(previous, serverSnapshot, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot], }); expect(afterEnrichment.snapshotSequence).toBe(20); expect(afterEnrichment.projects[0]?.title).toBe("Client ahead"); expect(afterEnrichment.projects[0]?.repositoryIdentity).toBeNull(); expect(afterEnrichment.threads[0]?.title).toBe("Local-only thread"); // Unmarked authoritative at the same lower sequence still performs the reset. const afterAuthoritative = mergeShellSnapshotProjects(afterEnrichment, serverSnapshot); expect(afterAuthoritative.snapshotSequence).toBe(3); expect(afterAuthoritative.projects[0]?.title).toBe("Server reset"); expect(afterAuthoritative.projects[0]?.repositoryIdentity).toBeNull(); expect(afterAuthoritative.threads[0]?.title).toBe("Server thread"); }); it("authoritative null retains identity then same-sequence marked enrichment clears it", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 9, projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }], threads: [{ ...v2ThreadShell, title: "Local-only thread" }], }; const serverSnapshot = { ...v2ShellSnapshot, snapshotSequence: 3, projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }], threads: [{ ...v2ThreadShell, title: "Server thread" }], }; const afterAuthoritative = mergeShellSnapshotProjects(previous, serverSnapshot); expect(afterAuthoritative.snapshotSequence).toBe(3); expect(afterAuthoritative.projects[0]?.title).toBe("Server reset"); expect(afterAuthoritative.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); expect(afterAuthoritative.threads[0]?.title).toBe("Server thread"); const afterEnrichment = mergeShellSnapshotProjects(afterAuthoritative, serverSnapshot, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot], }); expect(afterEnrichment.snapshotSequence).toBe(3); expect(afterEnrichment.projects[0]?.title).toBe("Server reset"); expect(afterEnrichment.projects[0]?.repositoryIdentity).toBeNull(); expect(afterEnrichment.threads[0]?.title).toBe("Server thread"); }); it("takes identity from a lower-sequence enrichment without reverting live thread state", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 5, projects: [{ ...v2Project, repositoryIdentity: null }], threads: [{ ...v2ThreadShell, title: "Newest title" }], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 4, projects: [{ ...v2Project, repositoryIdentity }], threads: [{ ...v2ThreadShell, title: "Stale title" }], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] }, ); expect(next.snapshotSequence).toBe(5); expect(next.threads[0]?.title).toBe("Newest title"); expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); }); it("treats higher-sequence compact enrichment as metadata-only", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 5, projects: [{ ...v2Project, repositoryIdentity: null }], threads: [{ ...v2ThreadShell, title: "Newest title" }], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 8, projects: [{ ...v2Project, repositoryIdentity }], threads: [], archivedThreads: [], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] }, ); expect(next.snapshotSequence).toBe(5); expect(next.threads[0]?.title).toBe("Newest title"); expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); }); it("lower-sequence enrichment cannot resurrect empty structural state", () => { const previous = { ...v2ShellSnapshot, snapshotSequence: 7, projects: [], threads: [], archivedThreads: [], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 4, projects: [{ ...v2Project, repositoryIdentity }], threads: [v2ThreadShell], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] }, ); expect(next.snapshotSequence).toBe(7); expect(next.projects).toEqual([]); expect(next.threads).toEqual([]); }); it("resolved null clears identity only for the marked root", () => { const otherProject = { ...v2Project, id: ProjectId.make("project-other"), workspaceRoot: "/workspace/other", repositoryIdentity: otherIdentity, }; const previous = { ...v2ShellSnapshot, snapshotSequence: 5, projects: [{ ...v2Project, repositoryIdentity }, otherProject], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 4, projects: [ { ...v2Project, repositoryIdentity: null }, { ...otherProject, repositoryIdentity: null }, ], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] }, ); expect(next.snapshotSequence).toBe(5); expect(next.projects).toHaveLength(2); expect(next.projects[0]?.repositoryIdentity).toBeNull(); expect(next.projects[1]?.repositoryIdentity).toEqual(otherIdentity); }); it("unrelated unresolved roots retain identity in the same enrichment snapshot", () => { const otherProject = { ...v2Project, id: ProjectId.make("project-other"), workspaceRoot: "/workspace/other", repositoryIdentity: otherIdentity, }; const previous = { ...v2ShellSnapshot, snapshotSequence: 3, projects: [{ ...v2Project, repositoryIdentity }, otherProject], threads: [{ ...v2ThreadShell, title: "Current thread" }], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 3, projects: [ { ...v2Project, repositoryIdentity }, { ...otherProject, repositoryIdentity: null }, ], threads: [{ ...v2ThreadShell, title: "Stale thread" }], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] }, ); expect(next.threads[0]?.title).toBe("Current thread"); expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity); expect(next.projects[1]?.repositoryIdentity).toEqual(otherIdentity); }); it("clears identity for multiple roots resolved in one enrichment batch", () => { const otherProject = { ...v2Project, id: ProjectId.make("project-other"), workspaceRoot: "/workspace/other", repositoryIdentity: otherIdentity, }; const previous = { ...v2ShellSnapshot, snapshotSequence: 3, projects: [{ ...v2Project, repositoryIdentity }, otherProject], }; const next = mergeShellSnapshotProjects( previous, { ...v2ShellSnapshot, snapshotSequence: 3, projects: [ { ...v2Project, repositoryIdentity: null }, { ...otherProject, repositoryIdentity: null }, ], }, { resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot, otherProject.workspaceRoot], }, ); expect(next.projects.map((project) => project.repositoryIdentity)).toEqual([null, null]); }); it("does not replace a live identity with one from an unmarked lower-sequence enrichment", () => { const liveIdentity = { canonicalKey: "github.com/example/live", locator: { source: "git-remote" as const, remoteName: "origin", remoteUrl: "https://github.com/example/live.git", }, }; const staleIdentity = { canonicalKey: "github.com/example/stale", locator: { source: "git-remote" as const, remoteName: "origin", remoteUrl: "https://github.com/example/stale.git", }, }; const next = mergeShellSnapshotProjects( { ...v2ShellSnapshot, snapshotSequence: 5, projects: [{ ...v2Project, repositoryIdentity: liveIdentity }], }, { ...v2ShellSnapshot, snapshotSequence: 4, projects: [{ ...v2Project, repositoryIdentity: staleIdentity }], }, { resolvedRepositoryIdentityRoots: [] }, ); expect(next.snapshotSequence).toBe(5); expect(next.projects[0]?.repositoryIdentity).toEqual(liveIdentity); }); it("keeps archive deltas out of the normal shell cache", () => { const archived = applyShellStreamEvent(v2ShellSnapshot, { kind: "thread.updated", sequence: 3, location: "archive", thread: { ...v2ThreadShell, archivedAt: v2ThreadShell.updatedAt }, }); expect(archived.threads).toEqual([]); expect(archived.archivedThreads).toEqual([]); const active = applyShellStreamEvent(archived, { kind: "thread.updated", sequence: 4, location: "active", thread: v2ThreadShell, }); expect(active.threads).toHaveLength(1); expect(active.archivedThreads).toEqual([]); }); it("removes a thread from either collection", () => { const next = applyShellStreamEvent(v2ShellSnapshot, { kind: "thread.removed", sequence: 5, location: "active", threadId: ThreadId.make(v2ThreadShell.id), }); expect(next.threads).toEqual([]); expect(next.snapshotSequence).toBe(5); }); it("leaves unknown future events unchanged", () => { const next = applyShellStreamEvent(v2ShellSnapshot, { kind: "future.event", sequence: 99, } as never); expect(next).toBe(v2ShellSnapshot); });});
import { ProjectId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
import { v2Project, v2ShellSnapshot, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
import { applyShellStreamEvent, mergeShellSnapshotProjects } from "./shellReducer.ts";
const repositoryIdentity = {
canonicalKey: "github.com/example/repo",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "https://github.com/example/repo.git",
},
};
const otherIdentity = {
canonicalKey: "github.com/example/other",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "https://github.com/example/other.git",
},
};
describe("applyShellStreamEvent", () => {
it("ignores stale project updates without mutating the snapshot", () => {
const snapshotWithProject = {
...v2ShellSnapshot,
snapshotSequence: 4,
projects: [v2Project],
};
for (const sequence of [3, 4]) {
const next = applyShellStreamEvent(snapshotWithProject, {
kind: "project.updated",
sequence,
project: { ...v2Project, title: "Stale Title" },
});
expect(next).toBe(snapshotWithProject);
expect(next.snapshotSequence).toBe(4);
expect(next.projects[0]?.title).toBe(v2Project.title);
}
});
it("applies project updates and removals", () => {
const updated = applyShellStreamEvent(v2ShellSnapshot, {
kind: "project.updated",
sequence: 1,
project: { ...v2Project, title: "Updated" },
});
expect(updated.projects[0]?.title).toBe("Updated");
expect(updated.snapshotSequence).toBe(1);
const removed = applyShellStreamEvent(updated, {
kind: "project.removed",
sequence: 2,
projectId: ProjectId.make(v2Project.id),
});
expect(removed.projects).toEqual([]);
});
it("keeps prior repositoryIdentity when a delta arrives with null identity", () => {
const withIdentity = applyShellStreamEvent(v2ShellSnapshot, {
kind: "project.updated",
sequence: 1,
project: { ...v2Project, repositoryIdentity },
});
expect(withIdentity.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
const withoutIdentity = applyShellStreamEvent(withIdentity, {
kind: "project.updated",
sequence: 2,
project: { ...v2Project, title: "Still same root", repositoryIdentity: null },
});
expect(withoutIdentity.projects[0]?.title).toBe("Still same root");
expect(withoutIdentity.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
});
it("does not keep prior repositoryIdentity after a workspace root move", () => {
const withIdentity = applyShellStreamEvent(v2ShellSnapshot, {
kind: "project.updated",
sequence: 1,
project: { ...v2Project, repositoryIdentity },
});
const moved = applyShellStreamEvent(withIdentity, {
kind: "project.updated",
sequence: 2,
project: {
...v2Project,
workspaceRoot: "/tmp/other-root",
repositoryIdentity: null,
},
});
expect(moved.projects[0]?.workspaceRoot).toBe("/tmp/other-root");
expect(moved.projects[0]?.repositoryIdentity).toBeNull();
});
it("merges full snapshot projects without dropping known repositoryIdentity", () => {
const previous = {
...v2ShellSnapshot,
projects: [{ ...v2Project, repositoryIdentity }],
};
const next = mergeShellSnapshotProjects(previous, {
...v2ShellSnapshot,
snapshotSequence: 2,
projects: [{ ...v2Project, title: "Refreshed", repositoryIdentity: null }],
});
expect(next.projects[0]?.title).toBe("Refreshed");
expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
});
it("authoritative lower-sequence reset replaces client-ahead structural state", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 9,
projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }],
threads: [{ ...v2ThreadShell, title: "Local-only thread" }],
};
const next = mergeShellSnapshotProjects(previous, {
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }],
threads: [{ ...v2ThreadShell, title: "Server thread" }],
});
expect(next.snapshotSequence).toBe(3);
expect(next.projects[0]?.title).toBe("Server reset");
expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
expect(next.threads[0]?.title).toBe("Server thread");
});
it("client-ahead survives lower marked enrichment then still resets on unmarked authoritative", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 20,
projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }],
threads: [{ ...v2ThreadShell, title: "Local-only thread" }],
};
const serverSnapshot = {
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }],
threads: [{ ...v2ThreadShell, title: "Server thread" }],
};
// Lower-sequence marked enrichment is identity-only and must not replace structure.
const afterEnrichment = mergeShellSnapshotProjects(previous, serverSnapshot, {
resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot],
});
expect(afterEnrichment.snapshotSequence).toBe(20);
expect(afterEnrichment.projects[0]?.title).toBe("Client ahead");
expect(afterEnrichment.projects[0]?.repositoryIdentity).toBeNull();
expect(afterEnrichment.threads[0]?.title).toBe("Local-only thread");
// Unmarked authoritative at the same lower sequence still performs the reset.
const afterAuthoritative = mergeShellSnapshotProjects(afterEnrichment, serverSnapshot);
expect(afterAuthoritative.snapshotSequence).toBe(3);
expect(afterAuthoritative.projects[0]?.title).toBe("Server reset");
expect(afterAuthoritative.projects[0]?.repositoryIdentity).toBeNull();
expect(afterAuthoritative.threads[0]?.title).toBe("Server thread");
});
it("authoritative null retains identity then same-sequence marked enrichment clears it", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 9,
projects: [{ ...v2Project, title: "Client ahead", repositoryIdentity }],
threads: [{ ...v2ThreadShell, title: "Local-only thread" }],
};
const serverSnapshot = {
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [{ ...v2Project, title: "Server reset", repositoryIdentity: null }],
threads: [{ ...v2ThreadShell, title: "Server thread" }],
};
const afterAuthoritative = mergeShellSnapshotProjects(previous, serverSnapshot);
expect(afterAuthoritative.snapshotSequence).toBe(3);
expect(afterAuthoritative.projects[0]?.title).toBe("Server reset");
expect(afterAuthoritative.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
expect(afterAuthoritative.threads[0]?.title).toBe("Server thread");
const afterEnrichment = mergeShellSnapshotProjects(afterAuthoritative, serverSnapshot, {
resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot],
});
expect(afterEnrichment.snapshotSequence).toBe(3);
expect(afterEnrichment.projects[0]?.title).toBe("Server reset");
expect(afterEnrichment.projects[0]?.repositoryIdentity).toBeNull();
expect(afterEnrichment.threads[0]?.title).toBe("Server thread");
});
it("takes identity from a lower-sequence enrichment without reverting live thread state", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 5,
projects: [{ ...v2Project, repositoryIdentity: null }],
threads: [{ ...v2ThreadShell, title: "Newest title" }],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 4,
projects: [{ ...v2Project, repositoryIdentity }],
threads: [{ ...v2ThreadShell, title: "Stale title" }],
},
{ resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] },
);
expect(next.snapshotSequence).toBe(5);
expect(next.threads[0]?.title).toBe("Newest title");
expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
});
it("treats higher-sequence compact enrichment as metadata-only", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 5,
projects: [{ ...v2Project, repositoryIdentity: null }],
threads: [{ ...v2ThreadShell, title: "Newest title" }],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 8,
projects: [{ ...v2Project, repositoryIdentity }],
threads: [],
archivedThreads: [],
},
{ resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] },
);
expect(next.snapshotSequence).toBe(5);
expect(next.threads[0]?.title).toBe("Newest title");
expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
});
it("lower-sequence enrichment cannot resurrect empty structural state", () => {
const previous = {
...v2ShellSnapshot,
snapshotSequence: 7,
projects: [],
threads: [],
archivedThreads: [],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 4,
projects: [{ ...v2Project, repositoryIdentity }],
threads: [v2ThreadShell],
},
{ resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] },
);
expect(next.snapshotSequence).toBe(7);
expect(next.projects).toEqual([]);
expect(next.threads).toEqual([]);
});
it("resolved null clears identity only for the marked root", () => {
const otherProject = {
...v2Project,
id: ProjectId.make("project-other"),
workspaceRoot: "/workspace/other",
repositoryIdentity: otherIdentity,
};
const previous = {
...v2ShellSnapshot,
snapshotSequence: 5,
projects: [{ ...v2Project, repositoryIdentity }, otherProject],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 4,
projects: [
{ ...v2Project, repositoryIdentity: null },
{ ...otherProject, repositoryIdentity: null },
],
},
{ resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] },
);
expect(next.snapshotSequence).toBe(5);
expect(next.projects).toHaveLength(2);
expect(next.projects[0]?.repositoryIdentity).toBeNull();
expect(next.projects[1]?.repositoryIdentity).toEqual(otherIdentity);
});
it("unrelated unresolved roots retain identity in the same enrichment snapshot", () => {
const otherProject = {
...v2Project,
id: ProjectId.make("project-other"),
workspaceRoot: "/workspace/other",
repositoryIdentity: otherIdentity,
};
const previous = {
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [{ ...v2Project, repositoryIdentity }, otherProject],
threads: [{ ...v2ThreadShell, title: "Current thread" }],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [
{ ...v2Project, repositoryIdentity },
{ ...otherProject, repositoryIdentity: null },
],
threads: [{ ...v2ThreadShell, title: "Stale thread" }],
},
{ resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot] },
);
expect(next.threads[0]?.title).toBe("Current thread");
expect(next.projects[0]?.repositoryIdentity).toEqual(repositoryIdentity);
expect(next.projects[1]?.repositoryIdentity).toEqual(otherIdentity);
});
it("clears identity for multiple roots resolved in one enrichment batch", () => {
const otherProject = {
...v2Project,
id: ProjectId.make("project-other"),
workspaceRoot: "/workspace/other",
repositoryIdentity: otherIdentity,
};
const previous = {
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [{ ...v2Project, repositoryIdentity }, otherProject],
};
const next = mergeShellSnapshotProjects(
previous,
{
...v2ShellSnapshot,
snapshotSequence: 3,
projects: [
{ ...v2Project, repositoryIdentity: null },
{ ...otherProject, repositoryIdentity: null },
],
},
{
resolvedRepositoryIdentityRoots: [v2Project.workspaceRoot, otherProject.workspaceRoot],
},
);
expect(next.projects.map((project) => project.repositoryIdentity)).toEqual([null, null]);
});
it("does not replace a live identity with one from an unmarked lower-sequence enrichment", () => {
const liveIdentity = {
canonicalKey: "github.com/example/live",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "https://github.com/example/live.git",
},
};
const staleIdentity = {
canonicalKey: "github.com/example/stale",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "https://github.com/example/stale.git",
},
};
const next = mergeShellSnapshotProjects(
{
...v2ShellSnapshot,
snapshotSequence: 5,
projects: [{ ...v2Project, repositoryIdentity: liveIdentity }],
},
{
...v2ShellSnapshot,
snapshotSequence: 4,
projects: [{ ...v2Project, repositoryIdentity: staleIdentity }],
},
{ resolvedRepositoryIdentityRoots: [] },
);
expect(next.snapshotSequence).toBe(5);
expect(next.projects[0]?.repositoryIdentity).toEqual(liveIdentity);
});
it("keeps archive deltas out of the normal shell cache", () => {
const archived = applyShellStreamEvent(v2ShellSnapshot, {
kind: "thread.updated",
sequence: 3,
location: "archive",
thread: { ...v2ThreadShell, archivedAt: v2ThreadShell.updatedAt },
});
expect(archived.threads).toEqual([]);
expect(archived.archivedThreads).toEqual([]);
const active = applyShellStreamEvent(archived, {
kind: "thread.updated",
sequence: 4,
location: "active",
thread: v2ThreadShell,
});
expect(active.threads).toHaveLength(1);
expect(active.archivedThreads).toEqual([]);
});
it("removes a thread from either collection", () => {
const next = applyShellStreamEvent(v2ShellSnapshot, {
kind: "thread.removed",
sequence: 5,
location: "active",
threadId: ThreadId.make(v2ThreadShell.id),
});
expect(next.threads).toEqual([]);
expect(next.snapshotSequence).toBe(5);
});
it("leaves unknown future events unchanged", () => {
const next = applyShellStreamEvent(v2ShellSnapshot, {
kind: "future.event",
sequence: 99,
} as never);
expect(next).toBe(v2ShellSnapshot);
});
});