/** * board 目录分页、树构建、祖先定位与筛选测试。请求由内存函数提供,不访问实际 workspace 服务。 * 列表顺序不保证父项先到;孤儿和循环节点仍须可访问,不能递归死循环。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { loadBoardDirectory, boardDirectory, activeBoardAncestors, filterBoardDirectory } from '../src/lib/board-directory.ts'; // 生成带 ID、slug 和 parent_board_id 的最小目录项,默认 title 使用 ID。 const board=(id,parent,title=id)=>({id,parent_board_id:parent,title,workspace_id:'w',kind:'board',version:1,slug:id+'-slug'}); // 第一页故意子先父后,下一页才给孙项;检查 include_subgraphs、offset 与 URL 编码,再检查层级、slug 祖先链和筛选路径。 test('directory includes subgraphs, traverses all pages and preserves nested ownership',async()=>{ const calls=[];const request=async(path)=>{calls.push(path);const offset=new URLSearchParams(path.split('?')[1]).get('offset');return offset==='0'?{items:[board('child','parent'),board('parent')],next_offset:100}:{items:[board('grandchild','child')],next_offset:null};}; const boards=await loadBoardDirectory(request,'space slug');assert.equal(calls.length,2);assert.ok(calls.every(path=>path.includes('include_subgraphs=true')));assert.ok(calls[1].endsWith('offset=100'));assert.ok(calls[0].includes('space%20slug')); const roots=boardDirectory(boards);assert.equal(roots.length,1);assert.equal(roots[0].board.id,'parent');assert.equal(roots[0].children[0].children[0].board.id,'grandchild'); assert.deepEqual([...activeBoardAncestors(boards,'grandchild-slug')],['grandchild','child','parent']); assert.deepEqual(filterBoardDirectory(roots,'grandchild')[0].children[0].children.map(item=>item.board.id),['grandchild']); assert.equal(filterBoardDirectory(roots,'parent')[0].children.length,1); }); // 父项缺失及双节点循环都提升为可见根项,子列表为空,避免丢失入口或无限嵌套。 test('missing parents and corrupt cycles remain accessible without recursive loops',()=>{ const roots=boardDirectory([board('orphan','missing'),board('a','b'),board('b','a')]);assert.deepEqual(roots.map(item=>item.board.id),['orphan','a','b']);assert.ok(roots.every(item=>item.children.length===0)); }); // 非递增 next_offset 明确拒绝;已经 abort 的信号由请求替身抛回,加载器不得吞掉取消。 test('directory rejects non-advancing pagination and propagates aborts',async()=>{ await assert.rejects(loadBoardDirectory(async()=>({items:[],next_offset:0}),'w'),/invalid_pagination/); const controller=new AbortController();controller.abort();await assert.rejects(loadBoardDirectory(async(_path,options)=>{options.signal.throwIfAborted();},'w',controller.signal),{name:'AbortError'}); });