2026-06-15 15:04:43 -07:00
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
|
import test from 'node:test';
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildContextPrefix,
|
|
|
|
|
|
buildMindSpaceChatContext,
|
2026-06-30 02:17:36 +08:00
|
|
|
|
buildSelectedAssetGreeting,
|
2026-06-15 15:04:43 -07:00
|
|
|
|
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 = '<h1>标题</h1><p>正文内容</p><script>alert(1)</script>';
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
mindspaceChatContextInternals.excerptContent(html, 'html'),
|
|
|
|
|
|
'标题 正文内容',
|
|
|
|
|
|
);
|
|
|
|
|
|
assert.equal(
|
|
|
|
|
|
mindspaceChatContextInternals.excerptContent('a'.repeat(20), 'markdown', 10),
|
|
|
|
|
|
`${'a'.repeat(9)}…`,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2026-06-30 02:17:36 +08:00
|
|
|
|
|
|
|
|
|
|
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',
|
2026-06-30 09:30:51 +08:00
|
|
|
|
agentSessionId: 'session-1',
|
2026-06-30 02:17:36 +08:00
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.match(prefix, /【界面勾选资料(硬性)】/);
|
|
|
|
|
|
assert.match(prefix, /sales\.xlsx(id: asset-9/);
|
2026-07-10 20:05:52 +08:00
|
|
|
|
assert.match(prefix, /【勾选资料读取(硬性)】/);
|
|
|
|
|
|
assert.match(prefix, /mindspace_asset_download/);
|
2026-06-30 09:30:51 +08:00
|
|
|
|
assert.match(prefix, /【勾选资料删除(硬性)】/);
|
|
|
|
|
|
assert.match(prefix, /mindspace_asset_delete/);
|
|
|
|
|
|
assert.match(prefix, /完整读写删改权限/);
|
2026-06-30 02:17:36 +08:00
|
|
|
|
assert.match(prefix, /指代「这个文件」「这份报告」「这个数据」时\*\*必须\*\*指该勾选资料/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 09:30:51 +08:00
|
|
|
|
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/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-30 02:17:36 +08:00
|
|
|
|
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',
|
|
|
|
|
|
}),
|
|
|
|
|
|
/这份报告/,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2026-07-11 00:23:01 +08:00
|
|
|
|
|
|
|
|
|
|
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, /数据表汇总校验/);
|
|
|
|
|
|
});
|