## 版本内容 ### 分享审计保留与清理 - 审计记录保留与清理能力:终态会话由所有者整体清空(写入墓碑事件保留追责线索)或导出完整 JSON 归档;终态满默认 90 天由 alarm 自动清理 - 审计保留期创建时自定义(7–365 天,白名单校验,默认 90) - 清理留痕集中展示:清理时间与方式(手动/自动)同步用户库(`audit_purged_at`/`audit_purge_type`),折叠面板展示;已清理分享等同删除效果,不再提供查看审计入口 - 手动清空后取消已排期的自动清理闹钟;保留期服务端校验统一为 7–365 天;修复闹钟设置失败导致"有排期无闹钟"的幽灵状态 ### 工程与质量 - 新增审计清理回归测试 8 用例(墓碑隔离、归属/终态校验、同步失败容错、排期回滚、留痕接缝、迁移幂等) - UserDBDO SQL 查询类型化封装(`query<T>`/`one<T>`),消除 22 处 `as unknown as` - 测试类型检查纳入门禁:`tests/` 拆分 `tsconfig.worker`/`tsconfig.frontend`(workers-types 与 DOM lib 的 Element 接口冲突无法单配置共存) - 弱随机数加固(会话名 `crypto.randomUUID`);pnpm 供应链策略(`blockExoticSubdeps`/`minimumReleaseAge` 7 天/`no-downgrade`);CI 权限收敛 `contents: read`;`.gitleaks.toml` 误报豁免 - pi-lens 项目策略口径(AGENTS.md #31):XSS 类规则刻意不做项目级禁用,仅行内逐处豁免 ## 提交列表 - 1c38c7a `release: 发布 v1.12.0 分享审计保留与清理完善版本` - 4dd7c63 `feat: 审计清理回归测试、UserDBDO 类型化查询与测试类型门禁` - 50389f3 `fix: 消除 pi-lens 剩余告警——SAFETY 注释/测试类型/XSS 抑制/空 catch 说明` - 23a3e8e `chore: 代码质量审查整改——弱随机数/测试类型检查/供应链加固等 7 项` - 1cf0ef9 `chore: 配置 pi-lens 项目策略降低风格类噪音并清理用户库遗留项` - 0f27a50 `fix: 审计清理后完全隐藏分享卡片,仅保留集中式清理记录` - 7c42e8e `feat: 审计清理改为删除效果并新增集中式清理记录留痕区` - a3847ee `fix: 审计清理后改用折叠面板展示清理记录并补齐中英文词条` - b730219 `fix: 审计保留期服务端校验统一为 7–365 天并修复清理闹钟静默失败` - a68a6c8 `feat: 分享审计保留期支持创建时自定义(7–365 天)` - 158b205 `fix: 手动清空审计后取消已排期的自动清理唤醒` - 618f63c `feat: 分享审计记录保留与清理能力(清空/导出/自动保留期)` ## 验证结果 - typecheck 全绿(src + tests.worker + tests.frontend + frontend 四段) - 全量测试 644/644(54 文件) - build:frontend 构建成功并内联 - 关联 Issue/PR:无(基于 v1.11.0 #102) --------- Co-authored-by: TanXin <tanxin@example.com>
124 lines
5.0 KiB
TypeScript
124 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
||
import { TerminalContext } from '../../src/worker/agent/terminal-context';
|
||
import { ToolExecutor } from '../../src/worker/agent/tool-executor';
|
||
|
||
// =====================================================================
|
||
// tool-executor.test.ts
|
||
// ---------------------------------------------------------------
|
||
// Agent 工具层的输出有界化回归:
|
||
// 1. docker_manage(logs) 必须是有界查看 —— 拒绝无限流式 -f/--follow,
|
||
// 未显式指定 --tail 时强制追加 --tail 200。
|
||
// 2. 所有 exec 类工具结果在进入 LLM 上下文前做 head/tail 截断,
|
||
// 防止大输出把后续每轮 LLM 请求体撑爆(弱网环境下的核心诱因)。
|
||
// =====================================================================
|
||
|
||
function makeExecutor(execResult: {
|
||
stdout: string;
|
||
stderr: string;
|
||
exitCode: number;
|
||
}) {
|
||
const execCommand = vi.fn(
|
||
async (command: string): Promise<typeof execResult> => execResult
|
||
);
|
||
const askConfirmation = vi.fn(async () => true);
|
||
const executor = new ToolExecutor(
|
||
new TerminalContext(),
|
||
execCommand,
|
||
askConfirmation,
|
||
undefined
|
||
);
|
||
return { executor, execCommand, askConfirmation };
|
||
}
|
||
|
||
describe('docker_manage(logs) — 日志查看必须是有界的', () => {
|
||
it('未指定 --tail 时强制追加 --tail 200', async () => {
|
||
const { executor, execCommand } = makeExecutor({ stdout: 'ok', stderr: '', exitCode: 0 });
|
||
const result = await executor.execute(
|
||
'docker_manage',
|
||
{ action: 'logs', target: 'blog-backend' },
|
||
undefined
|
||
);
|
||
expect(execCommand).toHaveBeenCalledOnce();
|
||
expect(execCommand.mock.calls[0][0]).toBe('docker logs --tail 200 blog-backend');
|
||
expect(JSON.parse(result).exit_code).toBe(0);
|
||
});
|
||
|
||
it('带其它选项(如 --since)时同样补齐 --tail', async () => {
|
||
const { executor, execCommand } = makeExecutor({ stdout: '', stderr: '', exitCode: 0 });
|
||
await executor.execute(
|
||
'docker_manage',
|
||
{ action: 'logs', target: 'blog-backend', options: '--since 24h' },
|
||
undefined
|
||
);
|
||
expect(execCommand.mock.calls[0][0]).toBe('docker logs --since 24h --tail 200 blog-backend');
|
||
});
|
||
|
||
it('已显式携带 --tail 时不重复追加', async () => {
|
||
const { executor, execCommand } = makeExecutor({ stdout: '', stderr: '', exitCode: 0 });
|
||
await executor.execute(
|
||
'docker_manage',
|
||
{ action: 'logs', target: 'blog-backend', options: '--tail 500' },
|
||
undefined
|
||
);
|
||
expect(execCommand.mock.calls[0][0]).toBe('docker logs --tail 500 blog-backend');
|
||
});
|
||
|
||
it('拒绝无限流式 -f / --follow:不执行任何命令并返回明确错误', async () => {
|
||
for (const options of ['-f', '--follow', '-f --tail 10', '--tail 10 --follow']) {
|
||
const { executor, execCommand } = makeExecutor({ stdout: '', stderr: '', exitCode: 0 });
|
||
const result = await executor.execute(
|
||
'docker_manage',
|
||
{ action: 'logs', target: 'blog-backend', options },
|
||
undefined
|
||
);
|
||
expect(execCommand).not.toHaveBeenCalled();
|
||
const parsed = JSON.parse(result);
|
||
expect(parsed.blocked).toBe(true);
|
||
expect(parsed.stderr).toContain('不允许');
|
||
}
|
||
});
|
||
|
||
it('含 shell 元字符的 options 会被净化后照常强制 --tail', async () => {
|
||
const { executor, execCommand } = makeExecutor({ stdout: '', stderr: '', exitCode: 0 });
|
||
await executor.execute(
|
||
'docker_manage',
|
||
{ action: 'logs', target: 'blog-backend', options: '--tail 5; rm -rf /' },
|
||
undefined
|
||
);
|
||
expect(execCommand.mock.calls[0][0]).toBe('docker logs --tail 200 blog-backend');
|
||
});
|
||
});
|
||
|
||
describe('工具结果进 LLM 前截断 — token budgeting', () => {
|
||
it('小输出原样返回,不带截断标记', async () => {
|
||
const { executor } = makeExecutor({ stdout: 'short output', stderr: '', exitCode: 0 });
|
||
const result = await executor.execute(
|
||
'execute_command',
|
||
{ command: 'echo short', timeout_ms: 5000 },
|
||
undefined
|
||
);
|
||
const parsed = JSON.parse(result);
|
||
expect(parsed.stdout).toBe('short output');
|
||
expect(parsed.truncated).toBeUndefined();
|
||
expect(parsed.exit_code).toBe(0);
|
||
});
|
||
|
||
it('大输出(200KB)截断到 64K 字符预算内,保留头尾并附省略计数', async () => {
|
||
const big = 'x'.repeat(200 * 1024);
|
||
const { executor } = makeExecutor({ stdout: big, stderr: '', exitCode: 0 });
|
||
const result = await executor.execute(
|
||
'execute_command',
|
||
{ command: 'cat big.log', timeout_ms: 5000 },
|
||
undefined
|
||
);
|
||
const parsed = JSON.parse(result);
|
||
expect(parsed.truncated).toBe(true);
|
||
expect(parsed.omitted_chars).toBeGreaterThan(0);
|
||
// 头部与尾部内容都保留
|
||
expect(parsed.stdout.startsWith('xxx')).toBe(true);
|
||
expect(parsed.stdout.endsWith('xxx')).toBe(true);
|
||
expect(parsed.stdout).toContain('输出过长已截断');
|
||
// 总长度受控(64K 预算 + 截断说明)
|
||
expect(parsed.stdout.length).toBeLessThan(70_000);
|
||
});
|
||
}); |