Address review comment 2159 on the temporary-notes UI work. Tests: - tree-model: cover handleCreate's race-guard temporaryExpiresAt patch — (a) server node inserted WITHOUT a deadline + create response carries one => node gains the deadline; (b) node already has a deadline => not overwritten, prev returned by reference. - ws-tree.service.spec: broadcastPageCreated now asserts the deadline is carried when present and pinned to null (`?? null`) when absent. - page-embed-query (new spec): syncTemporaryExpiresInCache patches the in-tree node's temporaryExpiresAt, and leaves the atom value at the same reference when the id is absent from the loaded tree (no write). Refactor (closes the drift bug-class at the root): - Client: extract one canonical pageToTreeNode(page, overrides) mapper in tree/utils and route buildTree, handleCreate's optimistic insert, the restore mutation and the duplicate handler through it. Restore stays permanent (server nulls temporaryExpiresAt) and duplicate stays permanent (server arms no timer) — both now reflect the server without a reload, where before they dropped the field entirely. - Server: extract one toTreeNodeSnapshot(page) helper called by both the PAGE_CREATED event enrichment (page.repo) and the addTreeNode broadcast (ws-tree.service), so the optional temporaryExpiresAt can't drift between the two literals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { getDefaultStore } from "jotai";
|
|
|
|
// Mock the app entry so importing the query module doesn't boot the whole app
|
|
// (it only needs queryClient's cache methods, which we stub here). The spies are
|
|
// declared via vi.hoisted so they exist before the hoisted vi.mock factory runs.
|
|
const { setQueryData, getQueryData, invalidateQueries } = vi.hoisted(() => ({
|
|
setQueryData: vi.fn(),
|
|
getQueryData: vi.fn(() => undefined as unknown),
|
|
invalidateQueries: vi.fn(),
|
|
}));
|
|
vi.mock("@/main.tsx", () => ({
|
|
queryClient: { setQueryData, getQueryData, invalidateQueries },
|
|
}));
|
|
|
|
import { syncTemporaryExpiresInCache } from "./page-embed-query";
|
|
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
|
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
|
|
|
const mkNode = (id: string, slugId: string): SpaceTreeNode =>
|
|
({
|
|
id,
|
|
slugId,
|
|
name: id,
|
|
position: "a0",
|
|
spaceId: "space-1",
|
|
parentPageId: null,
|
|
hasChildren: false,
|
|
children: [],
|
|
}) as unknown as SpaceTreeNode;
|
|
|
|
describe("syncTemporaryExpiresInCache — treeDataAtom patch", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
getQueryData.mockReturnValue(undefined);
|
|
});
|
|
|
|
it("patches the in-tree node's temporaryExpiresAt (sidebar marker updates without reload)", () => {
|
|
const store = getDefaultStore();
|
|
const tree = [mkNode("p1", "slug-1"), mkNode("p2", "slug-2")];
|
|
store.set(treeDataAtom, tree);
|
|
|
|
const deadline = "2026-07-01T00:00:00.000Z";
|
|
syncTemporaryExpiresInCache({ id: "p1", slugId: "slug-1" }, deadline);
|
|
|
|
const next = store.get(treeDataAtom);
|
|
// A new atom value was written...
|
|
expect(next).not.toBe(tree);
|
|
// ...the matching node gained the deadline...
|
|
expect(next.find((n) => n.id === "p1")?.temporaryExpiresAt).toBe(deadline);
|
|
// ...and the untouched sibling is unchanged.
|
|
expect(next.find((n) => n.id === "p2")?.temporaryExpiresAt).toBeUndefined();
|
|
});
|
|
|
|
it("leaves the atom value at the SAME reference when the id is absent from the tree (no write)", () => {
|
|
const store = getDefaultStore();
|
|
const tree = [mkNode("p1", "slug-1")];
|
|
store.set(treeDataAtom, tree);
|
|
|
|
syncTemporaryExpiresInCache(
|
|
{ id: "not-in-tree", slugId: "missing" },
|
|
"2026-07-01T00:00:00.000Z",
|
|
);
|
|
|
|
// treeModel.update is a no-op (same reference) for an unknown id, so the
|
|
// guard skips the store write entirely — same reference back.
|
|
expect(store.get(treeDataAtom)).toBe(tree);
|
|
});
|
|
});
|