Files
CloudSSH/tests/worker/agent-tool-executor.test.ts
TanXinandTanXin 52fec4e00f release: 发布 v1.12.0 分享审计保留与清理完善版本 (#103)
## 版本内容

### 分享审计保留与清理
- 审计记录保留与清理能力:终态会话由所有者整体清空(写入墓碑事件保留追责线索)或导出完整 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>
2026-08-26 12:35:58 +08:00

124 lines
5.0 KiB
TypeScript
Raw Permalink 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, 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);
});
});