test(temporary-notes): cover the create race-guard, broadcast deadline + cache patch; unify page->tree-node mappers

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>
This commit is contained in:
claude code agent 227
2026-06-27 00:58:40 +03:00
parent 12ff76fb89
commit d181b5c4ff
10 changed files with 289 additions and 72 deletions
@@ -9,26 +9,45 @@ export function sortPositionKeys(keys: any[]) {
});
}
/**
* Single canonical `IPage -> SpaceTreeNode` field mapper. Every place that
* materialises a tree node from a page (buildTree, the optimistic insert in
* handleCreate, restore, duplicate) routes through here so the field copy —
* crucially `temporaryExpiresAt` — can never silently drift between sites. The
* `overrides` cover the small per-site differences (e.g. `name: ""` for an
* optimistic create, `name: title || "Untitled"` for restore, `canEdit: true`
* for duplicate). The default `temporaryExpiresAt` comes straight off the page,
* so restore (which the server nulls) stays permanent and a temporary create
* keeps its clock marker without a reload.
*/
export function pageToTreeNode(
page: IPage,
overrides?: Partial<SpaceTreeNode>,
): SpaceTreeNode {
return {
id: page.id,
slugId: page.slugId,
name: page.title,
icon: page.icon,
position: page.position,
hasChildren: page.hasChildren,
spaceId: page.spaceId,
parentPageId: page.parentPageId,
canEdit: page.canEdit ?? page.permissions?.canEdit,
isTemplate: page.isTemplate,
temporaryExpiresAt: page.temporaryExpiresAt,
children: [],
...overrides,
};
}
export function buildTree(pages: IPage[]): SpaceTreeNode[] {
const pageMap: Record<string, SpaceTreeNode> = {};
const tree: SpaceTreeNode[] = [];
pages.forEach((page) => {
pageMap[page.id] = {
id: page.id,
slugId: page.slugId,
name: page.title,
icon: page.icon,
position: page.position,
hasChildren: page.hasChildren,
spaceId: page.spaceId,
parentPageId: page.parentPageId,
canEdit: page.canEdit ?? page.permissions?.canEdit,
isTemplate: page.isTemplate,
temporaryExpiresAt: page.temporaryExpiresAt,
children: [],
};
pageMap[page.id] = pageToTreeNode(page);
});
// Defense-in-depth: a duplicate id in `pages` would push two references to the