import assert from 'node:assert/strict'; import test from 'node:test'; import { buildContextPrefix, buildMindSpaceChatContext, buildSelectedAssetGreeting, formatContextChip, mindspaceChatContextInternals, } from './mindspace-chat-context.mjs'; const sampleSpace = { id: 'space-1', name: '我的空间', categories: [ { code: 'oa', name: 'OA 工作区', itemCount: 12 }, { code: 'draft', name: '页面草稿', itemCount: 5 }, ], }; const sampleCategory = { id: 'cat-oa', code: 'oa', name: 'OA 工作区', itemCount: 12, }; const samplePage = { id: 'page-1', title: '项目周报', summary: '本周项目进展与风险汇总', status: 'draft', versionNo: 3, categoryCode: 'draft', templateId: 'report', contentFormat: 'markdown', pageType: 'article', content: '# 周报\n\n- 完成 A 模块\n- 风险:进度偏紧', }; test('buildMindSpaceChatContext resolves home, category, and page views', () => { const home = buildMindSpaceChatContext({ space: sampleSpace, ownerUsername: 'john', selectedCategory: null, selectedPageId: null, pages: [samplePage], route: '/space', }); assert.equal(home.view, 'home'); assert.equal(home.ownerUsername, 'john'); assert.equal(home.homeCategories?.length, 2); assert.equal(home.recentPages?.[0]?.title, '项目周报'); const category = buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets: [ { id: 'asset-1', displayName: 'report.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, ], route: '/space?category=oa', }); assert.equal(category.view, 'category'); assert.deepEqual(category.category?.assets, [ { id: 'asset-1', displayName: 'report.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }, ]); const page = buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: null, selectedPageId: 'page-1', pages: [samplePage], pageLive: { page: samplePage, title: '项目周报(编辑中)', summary: samplePage.summary, content: samplePage.content, }, route: '/space/page/page-1', }); assert.equal(page.view, 'page'); assert.equal(page.page.title, '项目周报(编辑中)'); assert.match(page.page.contentExcerpt ?? '', /完成 A 模块/); assert.equal(page.category?.code, 'draft'); }); test('buildContextPrefix includes page excerpt and location hint', () => { const prefix = buildContextPrefix( buildMindSpaceChatContext({ space: sampleSpace, ownerUsername: 'john', selectedCategory: null, selectedPageId: 'page-1', pages: [samplePage], pageLive: { page: samplePage, title: samplePage.title, summary: samplePage.summary, content: samplePage.content, }, route: '/space/page/page-1', }), ); assert.match(prefix, /- 账号:john/); assert.match(prefix, /- 页面摘要:本周项目进展与风险汇总/); assert.match(prefix, /- 页面模板:分析报告/); assert.match(prefix, /- 当前正文摘录/); assert.match(prefix, /完成 A 模块/); assert.match(prefix, /【定位说明】/); assert.match(prefix, /指代「这个页面」「改这里」时默认指该页面/); assert.match(prefix, /\n---\n\n$/); }); test('buildContextPrefix lists category assets on category view', () => { const prefix = buildContextPrefix( buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets: [{ id: 'asset-1', displayName: 'report.xlsx', mimeType: 'text/csv' }], route: '/space?category=oa', }), ); assert.match(prefix, /- 分类:OA 工作区(oa,共 12 项)/); assert.match(prefix, /report\.xlsx(id: asset-1/); assert.match(prefix, /指代「这里的文件」时默认指该分类下的资料/); }); test('formatContextChip prefers page, then category with count, then space name', () => { assert.equal( formatContextChip({ spaceName: '我的空间', view: 'page', route: '/space/page/page-1', page: { id: 'page-1', title: '项目周报', versionNo: 3 }, }), '项目周报 · v3', ); assert.equal( formatContextChip({ spaceName: '我的空间', view: 'category', route: '/space?category=oa', category: { code: 'oa', name: 'OA 工作区', itemCount: 12 }, }), 'OA 工作区 · 12 项', ); }); test('excerptContent strips html and truncates long text', () => { const html = '
正文内容
'; assert.equal( mindspaceChatContextInternals.excerptContent(html, 'html'), '标题 正文内容', ); assert.equal( mindspaceChatContextInternals.excerptContent('a'.repeat(20), 'markdown', 10), `${'a'.repeat(9)}…`, ); }); test('buildMindSpaceChatContext exposes single selected asset as focused asset', () => { const asset = { id: 'asset-9', displayName: 'Q1-report.pdf', mimeType: 'application/pdf', filename: 'Q1-report.pdf', sizeBytes: 1200, categoryCode: 'oa', assetType: 'file', }; const context = buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets: [asset], selectedAssets: [asset], route: '/space?category=oa', }); assert.equal(context.selectedAssets?.length, 1); assert.equal(context.focusedAsset?.id, 'asset-9'); assert.equal(formatContextChip(context), '已选 · Q1-report.pdf'); }); test('buildContextPrefix binds deictic references to a single checked asset', () => { const asset = { id: 'asset-9', displayName: 'sales.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', filename: 'sales.xlsx', categoryCode: 'oa', }; const prefix = buildContextPrefix( buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets: [asset], selectedAssets: [asset], route: '/space?category=oa', h5ApiBase: 'http://127.0.0.1:8081', agentSessionId: 'session-1', }), ); assert.match(prefix, /【界面勾选资料(硬性)】/); assert.match(prefix, /sales\.xlsx(id: asset-9/); assert.match(prefix, /【勾选资料读取(硬性)】/); assert.match(prefix, /mindspace_asset_download/); assert.match(prefix, /【勾选资料删除(硬性)】/); assert.match(prefix, /mindspace_asset_delete/); assert.match(prefix, /完整读写删改权限/); assert.match(prefix, /指代「这个文件」「这份报告」「这个数据」时\*\*必须\*\*指该勾选资料/); }); test('buildContextPrefix includes delete fallback when agent session id is missing', () => { const asset = { id: 'asset-9', displayName: 'sales.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }; const prefix = buildContextPrefix( buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets: [asset], selectedAssets: [asset], route: '/space?category=oa', h5ApiBase: 'http://127.0.0.1:8081', }), ); assert.match(prefix, /CURRENT_AGENT_SESSION/); }); test('buildContextPrefix requires clarification when multiple assets are checked', () => { const assets = [ { id: 'asset-a', displayName: 'a.pdf', mimeType: 'application/pdf' }, { id: 'asset-b', displayName: 'b.csv', mimeType: 'text/csv' }, ]; const prefix = buildContextPrefix( buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets, selectedAssets: assets, route: '/space?category=oa', }), ); assert.match(prefix, /同时勾选了 2 份资料/); assert.match(prefix, /必须先追问/); assert.doesNotMatch(prefix, /当前聚焦资料/); assert.equal(formatContextChip(buildMindSpaceChatContext({ space: sampleSpace, selectedCategory: sampleCategory, selectedPageId: null, pages: [], assets, selectedAssets: assets, route: '/space?category=oa', })), '已选 2 项资料'); }); test('buildSelectedAssetGreeting adapts wording to asset kind', () => { assert.match( buildSelectedAssetGreeting({ displayName: 'sales.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }), /这份数据表/, ); assert.match( buildSelectedAssetGreeting({ displayName: 'review.pdf', mimeType: 'application/pdf', }), /这份报告/, ); }); test('buildMindSpaceChatContext reconciles stale category itemCount with loaded assets', () => { const staleCategory = { ...sampleCategory, itemCount: 0 }; const asset = { id: 'asset-1', displayName: 'report.xlsx', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', workspaceRelativePath: 'oa/report.xlsx', }; const context = buildMindSpaceChatContext({ space: { ...sampleSpace, categories: [{ ...sampleCategory, itemCount: 0 }], }, selectedCategory: staleCategory, selectedPageId: null, pages: [], assets: [asset], selectedAssets: [asset], route: '/space?category=oa', h5ApiBase: 'http://127.0.0.1:8081', agentSessionId: 'session-1', }); assert.equal(context.category?.itemCount, 1); const prefix = buildContextPrefix(context); assert.match(prefix, /OA 工作区(oa,共 1 项)/); assert.match(prefix, /已落盘 `oa\/report\.xlsx`/); assert.match(prefix, /数据表汇总校验/); });