Files
Genarrative/apps/ai-game-creator-shell/tests/directThreadSanitize.test.ts
T
k88936 d07f894bf6 修复:Bearer 引号值脱敏正则只匹配到第一个空格,密钥尾部漏出
- directThreadSanitize.ts:BEARER_VALUE 的引号分支由 \\.?["'] 改为 \\?["'],与同文件 SENSITIVE_ASSIGNMENT 的写法一致
- 原写法要求引号前必须有一个字面反斜杠,导致 Bearer "abc def" 走不到引号分支,退化到裸 token 分支后只遮到第一个空格,示例会输出 Bearer [redacted-secret] def"
- 修正后引号内的整段值都被遮掉;JSON 转义的 \"abc def\" 仍然匹配
- 同步在 directThreadSanitize.test.ts 增加带引号 Bearer 的用例(修复前该用例失败)
2026-10-03 13:42:40 +08:00

142 lines
4.3 KiB
TypeScript

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 <absolute-path>',
);
expect(sanitizeDirectThreadText(ROOT, 'file:///etc/passwd')).toBe(
'<absolute-path>',
);
expect(sanitizeDirectThreadText(ROOT, 'win C:\\Users\\me\\x.txt')).toBe(
'win <absolute-path>',
);
});
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 <absolute-path>')).toBe(
'at <absolute-path>',
);
});
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 <absolute-path>',
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 <absolute-path>',
});
});
});