import { describe, expect, it } from 'vitest'; import { sanitizeDirectThreadEvent, sanitizeDirectThreadText, } from '../src/view/project-development/chat/conversation/directThreadSanitize'; import type { ThreadEvent } from '../src/view/project-development/chat/generated/ThreadEvent'; const ROOT = '/workspace/direct-project'; describe('directThreadSanitize', () => { it('项目内绝对路径归一成项目相对路径', () => { expect( sanitizeDirectThreadText( ROOT, 'open /workspace/direct-project/game/src/x.ts', ), ).toBe('open game/src/x.ts'); }); it('项目外绝对路径与 file:// 换成占位符', () => { expect(sanitizeDirectThreadText(ROOT, 'at /root/secret/x')).toBe( 'at ', ); expect(sanitizeDirectThreadText(ROOT, 'file:///etc/passwd')).toBe( '', ); expect(sanitizeDirectThreadText(ROOT, 'win C:\\Users\\me\\x.txt')).toBe( 'win ', ); }); it('有前缀的密钥与 JWT 换成占位符', () => { const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abcdefgh'; expect(sanitizeDirectThreadText(ROOT, 'key=sk-abcdefghijklmnop')).toBe( 'key=[redacted-secret]', ); expect(sanitizeDirectThreadText(ROOT, `token ${jwt}`)).toBe( 'token [redacted-secret]', ); }); it('敏感赋值只遮挡值,保留同行其它信息', () => { expect(sanitizeDirectThreadText(ROOT, 'password=hunter2 status=401')).toBe( '[redacted-sensitive-field]=[redacted-secret] status=401', ); expect(sanitizeDirectThreadText(ROOT, '"token": "raw-value"')).toBe( '"[redacted-sensitive-field]": [redacted-secret]', ); }); it('Bearer 值换成占位符', () => { expect(sanitizeDirectThreadText(ROOT, 'Authorization: Bearer abc123')).toBe( '[redacted-sensitive-field]: [redacted-secret]', ); expect(sanitizeDirectThreadText(ROOT, 'h x Bearer abc123 y')).toBe( 'h x Bearer [redacted-secret] y', ); // 带引号的 Bearer 值(含空格)也要整段遮掉,不能只遮到第一个空格。 expect(sanitizeDirectThreadText(ROOT, 'h x Bearer "abc def" y')).toBe( 'h x Bearer [redacted-secret] y', ); }); it('.env / game-creator.config 换成配置占位符', () => { expect(sanitizeDirectThreadText(ROOT, 'cat .env.local')).toBe( 'cat [redacted-config]', ); }); it('私钥块整块吞掉', () => { const pem = [ '-----BEGIN PRIVATE KEY-----', 'MIIEvQIBADANBg', '-----END PRIVATE KEY-----', '', ].join('\n'); expect(sanitizeDirectThreadText(ROOT, pem)).toBe( '[redacted sensitive context]\n', ); }); it('敏感开关整行遮挡', () => { expect( sanitizeDirectThreadText(ROOT, 'curl --token abc123 https://x'), ).toBe('[redacted sensitive context]'); }); it('已有占位符不再二次变形', () => { expect(sanitizeDirectThreadText(ROOT, 'at ')).toBe( 'at ', ); }); it('多行文本保留换行', () => { const value = '第一段\nkey=sk-abcdefghijklmnop\n第三段\n'; expect(sanitizeDirectThreadText(ROOT, value)).toBe( '第一段\nkey=[redacted-secret]\n第三段\n', ); }); it('条目事件只改自由文本字段,身份与时间原样', () => { const event: ThreadEvent = { type: 'item.completed', item: { itemType: 'message', itemId: 'msg-1', role: 'assistant', text: 'key=sk-abcdefghijklmnop at /root/secret/x', at: 1_700_000_000_000, }, at: 1_700_000_000_100, }; expect(sanitizeDirectThreadEvent(ROOT, event)).toEqual({ type: 'item.completed', item: { itemType: 'message', itemId: 'msg-1', role: 'assistant', text: 'key=[redacted-secret] at ', at: 1_700_000_000_000, }, at: 1_700_000_000_100, }); }); it('流式增量事件遮挡正文', () => { const event: ThreadEvent = { type: 'item.delta', itemId: 'msg-1', kind: 'message', delta: 'see /root/secret/x', }; expect(sanitizeDirectThreadEvent(ROOT, event)).toEqual({ type: 'item.delta', itemId: 'msg-1', kind: 'message', delta: 'see ', }); }); });