diff --git a/src/app/conversations/page.tsx b/src/app/conversations/page.tsx index 918282d..191e69b 100644 --- a/src/app/conversations/page.tsx +++ b/src/app/conversations/page.tsx @@ -7,7 +7,7 @@ import { StatusBar, } from "@/components/app-ui"; import { requirePageSession } from "@/lib/boss-auth"; -import { getConversationHomeItems } from "@/lib/boss-projections"; +import { getConversationWebItems } from "@/lib/boss-projections"; import { readState } from "@/lib/boss-data"; export const dynamic = "force-dynamic"; @@ -15,7 +15,7 @@ export const dynamic = "force-dynamic"; export default async function ConversationsPage() { await requirePageSession(); const state = await readState(); - const conversations = getConversationHomeItems(state); + const conversations = getConversationWebItems(state); return ( diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts index 588705e..a726af6 100644 --- a/src/lib/boss-projections.ts +++ b/src/lib/boss-projections.ts @@ -660,6 +660,14 @@ export function getConversationHomeItems(state: BossState): ConversationItem[] { return sortConversationItems(passthrough); } +export function getConversationWebItems(state: BossState): ConversationItem[] { + return getConversationHomeItems(state).map((item) => ({ + ...item, + topPinnedLabel: undefined, + manualPinned: false, + })); +} + export function getConversationHomeItemForProject(state: BossState, projectId: string): ConversationItem | null { const normalizedProjectId = projectId.trim(); if (!normalizedProjectId) { diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index 84d23ea..eac0758 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -8,6 +8,7 @@ let runtimeRoot = ""; let readState: (typeof import("../src/lib/boss-data"))["readState"]; let updateConversationAction: (typeof import("../src/lib/boss-data"))["updateConversationAction"]; let getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"]; +let getConversationWebItems: (typeof import("../src/lib/boss-projections"))["getConversationWebItems"]; let getConversationHomeItemForProject: (typeof import("../src/lib/boss-projections"))["getConversationHomeItemForProject"]; let getConversationThreadItemForProject: (typeof import("../src/lib/boss-projections"))["getConversationThreadItemForProject"]; let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"]; @@ -31,6 +32,7 @@ async function setup() { readState = data.readState; updateConversationAction = data.updateConversationAction; getConversationHomeItems = projections.getConversationHomeItems; + getConversationWebItems = projections.getConversationWebItems; getConversationHomeItemForProject = projections.getConversationHomeItemForProject; getConversationThreadItemForProject = projections.getConversationThreadItemForProject; getConversationFolderView = projections.getConversationFolderView; @@ -770,6 +772,65 @@ test("homepage rows do not expose pinned labels in the Web surface", async () => assert.equal(getConversationPinnedBadgeLabel(masterAgent!), ""); }); +test("web conversation projection strips pin metadata before rendering the Mac/Web surface", async () => { + await setup(); + const state = await readState(); + + state.projects = state.projects.filter((project) => project.id === "master-agent"); + state.projects.push( + { + ...buildImportedThreadProject( + "mac-studio", + "solo-pinned-thread", + "Solo", + "solo", + "单线程置顶验证", + "solo-thread", + "2026-03-30T13:00:00+08:00", + ), + pinned: true, + }, + { + ...buildImportedThreadProject( + "mac-studio", + "boss-thread-1", + "Boss", + "boss", + "归档确认", + "thread-1", + "2026-03-30T11:00:00+08:00", + ), + pinned: true, + }, + buildImportedThreadProject( + "mac-studio", + "boss-thread-2", + "Boss", + "boss", + "发布回滚", + "thread-2", + "2026-03-30T12:00:00+08:00", + ), + ); + + const webItems = getConversationWebItems(state); + const pinnedThread = webItems.find((item) => item.projectId === "solo-pinned-thread"); + const folder = webItems.find((item) => item.conversationType === "folder_archive"); + const masterAgent = webItems.find((item) => item.projectId === "master-agent"); + + assert.ok(pinnedThread, "expected pinned thread in web projection"); + assert.equal(pinnedThread?.topPinnedLabel, undefined); + assert.equal(pinnedThread?.manualPinned, false); + + assert.ok(folder, "expected folder archive in web projection"); + assert.equal(folder?.topPinnedLabel, undefined); + assert.equal(folder?.manualPinned, false); + + assert.ok(masterAgent, "expected master agent in web projection"); + assert.equal(masterAgent?.topPinnedLabel, undefined); + assert.equal(masterAgent?.manualPinned, false); +}); + test("folder archive action path encodes folder keys with nested path segments", async () => { await setup(); diff --git a/tests/conversation-web-surface.test.ts b/tests/conversation-web-surface.test.ts new file mode 100644 index 0000000..af82339 --- /dev/null +++ b/tests/conversation-web-surface.test.ts @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import path from "node:path"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const testsDir = path.dirname(fileURLToPath(import.meta.url)); + +async function readWorkspaceFile(relativePath: string) { + return readFile(path.join(testsDir, "..", relativePath), "utf8"); +} + +test("web conversations page uses a web-specific projection without pin metadata", async () => { + const [pageSource, projectionsSource] = await Promise.all([ + readWorkspaceFile("src/app/conversations/page.tsx"), + readWorkspaceFile("src/lib/boss-projections.ts"), + ]); + + assert.match( + projectionsSource, + /export function getConversationWebItems\(/, + "expected a web-specific conversation projection helper", + ); + assert.match( + pageSource, + /const conversations = getConversationWebItems\(state\);/, + "expected the web conversations page to use the web-specific projection", + ); +});