Files
makelore/tests/unit/cloud-delivered-files.test.ts
T

137 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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([
'![完成的图片](/outputs/drawing.png) [文件][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);
});
});