137 lines
5.6 KiB
TypeScript
137 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { deliveredFilePaths } from '@/pages/CloudAgents/cloud-delivered-files';
|
||
import type { CloudHistory, CloudMessage } from '../../shared/cloud-agents';
|
||
|
||
const threadId = 'thread-1';
|
||
function message(content: string, role: CloudMessage['role'] = 'assistant', extra: Partial<CloudMessage> = {}): CloudMessage {
|
||
return { id: 1, role, content, request_id: 'request-1', run_id: 'run-1', created_at: '2026-10-12T00:00:00Z', ...extra };
|
||
}
|
||
function history(...messages: CloudMessage[]): CloudHistory {
|
||
return { thread_id: threadId, messages, run: null, next_offset: null };
|
||
}
|
||
function paths(content: string): string[] {
|
||
return [...deliveredFilePaths(history(message(content)), threadId)];
|
||
}
|
||
|
||
describe('saved assistant file delivery evidence', () => {
|
||
it.each([
|
||
['sandbox:/mnt/data/report.xlsx', '/mnt/data/report.xlsx'],
|
||
['outputs/story.txt', '/outputs/story.txt'],
|
||
['/report.pdf', '/report.pdf'],
|
||
['/api/chat/thread/thread-1/artifacts/outputs/report.pdf', '/outputs/report.pdf'],
|
||
['/api/makelore/threads/thread-1/artifacts/outputs/report.pdf', '/outputs/report.pdf'],
|
||
['sandbox:/outputs/%E6%95%85%E4%BA%8B%20%E7%BB%88%E7%A8%BF.txt', '/outputs/故事 终稿.txt'],
|
||
['/api/makelore/threads/thread-1/artifacts/%E8%A1%A8%E6%A0%BC%20%E7%BB%88%E7%A8%BF.xlsx', '/表格 终稿.xlsx'],
|
||
])('normalizes an explicit local deliverable %s', (target, expected) => {
|
||
expect(paths(`[保存成果](${target})`)).toEqual([expected]);
|
||
});
|
||
|
||
it('accepts actual link and image references, resolves case and whitespace, and ignores unused definitions', () => {
|
||
expect(paths([
|
||
'[故事][My File] 和 ![封面][cover],还有 [快捷成果][]。',
|
||
'',
|
||
'[my file]: sandbox:/outputs/story.txt',
|
||
'[COVER]: /outputs/cover.png',
|
||
'[快捷成果]: /outputs/quick.pdf',
|
||
'[unused]: /internal/unmentioned.json',
|
||
].join('\n'))).toEqual(['/outputs/story.txt', '/outputs/cover.png', '/outputs/quick.pdf']);
|
||
});
|
||
|
||
it('accepts inline images and follows the first reference definition', () => {
|
||
expect(paths([
|
||
' [文件][file]',
|
||
'',
|
||
'[file]: /outputs/final.txt',
|
||
'[file]: /internal/duplicate.txt',
|
||
].join('\n'))).toEqual(['/outputs/drawing.png', '/outputs/final.txt']);
|
||
});
|
||
|
||
it('keeps explicitly delivered code and data files regardless of their name or extension and deduplicates paths', () => {
|
||
expect(paths([
|
||
'[脚本](/batch.py) [数据](/result.json) [运行文件](/output.pyc)',
|
||
'[再次保存](sandbox:/batch.py) [重复](batch.py)',
|
||
].join('\n'))).toEqual(['/batch.py', '/result.json', '/output.pyc']);
|
||
});
|
||
|
||
it('uses only assistant replies and never user text, system text, or reasoning', () => {
|
||
const saved = history(
|
||
message('[用户资料](/user-link.pdf)', 'user'),
|
||
message('[系统文件](/system.json)', 'system'),
|
||
message('[已交付](/final.pdf)', 'assistant', { reasoning_content: '[工作记录](/thinking.json)' }),
|
||
);
|
||
expect([...deliveredFilePaths(saved, threadId)]).toEqual(['/final.pdf']);
|
||
});
|
||
|
||
it('does not resolve definitions from a different message', () => {
|
||
expect([...deliveredFilePaths(history(
|
||
message('[file]: /internal/only-defined.txt'),
|
||
message('[没有本条定义的文件][file]', 'assistant', { id: 2 }),
|
||
), threadId)]).toEqual([]);
|
||
});
|
||
|
||
it('ignores raw paths, fenced and indented code, inline code, raw HTML and code examples', () => {
|
||
expect(paths([
|
||
'过程位置 /internal/log.json,outputs/script.py,sandbox:/outputs/raw.pdf。',
|
||
'',
|
||
'```markdown',
|
||
'[示例](/internal/fenced.txt)',
|
||
'[hidden]: /internal/fenced-reference.txt',
|
||
'```',
|
||
'',
|
||
' [示例](/internal/indented.txt)',
|
||
'',
|
||
'`[示例](/internal/inline.txt)`',
|
||
'',
|
||
'<a href="/internal/html.txt">HTML 示例</a>',
|
||
'<img src="/internal/html.png" />',
|
||
'',
|
||
'[不存在的引用][hidden]',
|
||
'',
|
||
'[真正的成果](/outputs/final.docx)',
|
||
].join('\n'))).toEqual(['/outputs/final.docx']);
|
||
});
|
||
|
||
it.each([
|
||
'https://example.com/report.pdf',
|
||
'//example.com/report.pdf',
|
||
'mailto:person@example.com',
|
||
'javascript:alert(1)',
|
||
'data:text/plain,hello',
|
||
'file:///private/file.txt',
|
||
'C:/private/file.txt',
|
||
'#section',
|
||
'/report.pdf?download=true',
|
||
'/report.pdf#section',
|
||
'/report.pdf%3Fdownload=true',
|
||
'/report.pdf%23section',
|
||
'/../private.txt',
|
||
'outputs/../private.txt',
|
||
'/outputs/%2e%2e/private.txt',
|
||
'/outputs/%252e%252e/private.txt',
|
||
'/outputs%2f..%2fprivate.txt',
|
||
'/outputs/./report.txt',
|
||
'/outputs\\private.txt',
|
||
'/outputs/%5Cprivate.txt',
|
||
'/outputs/%255Cprivate.txt',
|
||
'/outputs/%00private.txt',
|
||
'/outputs/\u0000private.txt',
|
||
'/outputs/%0Aprivate.txt',
|
||
'/outputs/%E0%A4%A',
|
||
'sandbox://example.com/report.txt',
|
||
'/outputs/',
|
||
'/api/chat/thread/other-thread/artifacts/final.pdf',
|
||
'/api/makelore/threads/other-thread/artifacts/final.pdf',
|
||
'/api/chat/thread/thread-1/artifacts/',
|
||
'/api/makelore/threads/thread-1/attachments/final.pdf',
|
||
])('rejects an unsafe, ambiguous, external, or cross-thread target %s', target => {
|
||
expect(paths(`[文件](<${target}>)`)).toEqual([]);
|
||
});
|
||
|
||
it('returns no evidence until matching conversation history exists', () => {
|
||
expect(deliveredFilePaths(null, threadId).size).toBe(0);
|
||
expect(deliveredFilePaths(undefined, threadId).size).toBe(0);
|
||
expect(deliveredFilePaths(history(message('[文件](/final.pdf)')), 'other-thread').size).toBe(0);
|
||
expect(deliveredFilePaths(history(message('[文件](/final.pdf)')), '').size).toBe(0);
|
||
});
|
||
});
|