news 2026/9/10 8:55:43

AionUi 中 Aion CLI(aionrs)E2E 测试需求全解析:从维度设计、用例矩阵到数据库验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AionUi 中 Aion CLI(aionrs)E2E 测试需求全解析:从维度设计、用例矩阵到数据库验证

AionUi 中 Aion CLI(aionrs)E2E 测试需求全解析:从维度设计、用例矩阵到数据库验证

【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi

AionUi 桌面端内置了对 Aion CLI(aionrs)agent 的支持,用户可以在 guid 首页选择 aionrs、配置上下文与权限模式后发起对话,并通过进程端以 stdin/stdout JSON Lines 协议驱动 aionrs binary 完成流式回复。本文以仓库中 chat-aionrs 测试需求文档 为主体,结合源码实现与 Gate 2 用例文档,完整拆解 aionrs 的端到端测试需求:五维测试矩阵(文件夹关联、文件上传、模型、权限、对话中切换)、binary 前置条件与超时策略、临时目录清理契约、数据库验证字段结构,以及>const { providers: allProviders, getAvailableModels, formatModelLabel } = useModelProviderList(); // AionCore does not support Google Auth — filter it out const providers = useMemo( () => allProviders.filter((p) => !p.platform?.toLowerCase().includes('gemini-with-google-auth')), [allProviders] );

provider 列表的底层来源是 useModelProviderList.ts,它通过useSWR拉取ipcBridge.mode.listProviders.invoke()返回的用户配置 provider 列表(并额外过滤p.enabled !== false的禁用项),即用户配置文件存储的 provider 列表,而非动态探测。

档位定义(runtime 动态决定,不可 hardcode):

  1. 档位 1 - 默认模型:E2E setup 查询ipcBridge.mode.getModelConfig.invoke(),过滤gemini-with-google-auth,取providers[0].model[0](第一个可用 provider 的第一个 model)
  2. 档位 2 - 切换模型:同一 provider 下的不同 model(providers[0].model[1]),或跨 provider(providers[1].model[0],若存在第二个 provider)

E2E 前置条件

  • 至少 1 个非 Google Auth provider
  • 该 provider 至少有 2 个可用 model(用于切换测试)
  • 若只有 1 个 model,测试降级为"只验证当前 model,不测切换"

验证策略

  • 通过invokeBridge(page, 'conversation.get', { id })查询 DB
  • 校验conversations.model字段(模型 ID)
  • 校验conversations.extra.model(JSON)包含{ id, useModel, name, ... }

已知约束:aionrs 不支持 Google Auth;模型切换行为 E2E 只验证 DB 字段更新,不验证 binary 重启(见 §8 议题 1 决策)。

2.5 权限模式(3 档)

档位枚举由 aionrs runtime capabilities 上报:

aionrs: [ { value: 'default', label: 'Default' }, { value: 'auto_edit', label: 'Auto-Accept Edits' }, { value: 'yolo', label: 'YOLO' }, ];
modelabel行为
defaultDefault每次工具调用需确认
auto_editAuto-Accept Edits自动批准 edit / info 类工具,exec / mcp 仍需确认
yoloYOLO全自动批准所有工具

权限切换接口:

  • guid 页:GuidActionRow.tsxAgentModeSelector组件
  • 对话页:AionrsSendBox.tsx— 同样使用AgentModeSelector
  • 进程端:setMode()更新 DB + 发送set_mode到 binary

AgentModeSelector.tsx 中可以看到 aionrs 权限选项的 testid 落地(data-testid={'aionrs-mode-option-' + mode.value}),说明 Gate 2/3 阶段的 testid 补充工作已部分反映在源码中。

验证策略:查询 DBSELECT json_extract(extra, '$.sessionMode') FROM conversations WHERE id = ?,期望值'default' | 'auto_edit' | 'yolo'

测试覆盖:guid 页选 3 种模式各 1 次(3 个用例)+ 对话中切换 3 种模式。

2.6 对话中切换(必测)

  • 切换模型:点击对话页AionrsModelSelector按钮 → 选择不同模型;预期下次发送消息时生效(是否需重启 binary 待 E2E 探测);验证 DBconversations.extra.model.useModel
  • 切换权限:点击对话页AgentModeSelector按钮 → 选择不同权限;预期立即生效(setMode()发送set_mode到 binary);验证 DBconversations.extra.sessionMode
  • 边界场景:工具确认弹窗中途切换权限 → 当前行为待探测(是否取消 pending 确认?)

2.7 用例矩阵(正交覆盖)

全排列为 2(文件夹)× 2(上传)× 2(模型)× 3(权限)=24 组合,通过收敛策略缩减为 11-17 个推荐用例:

  • P0 核心用例(5 个):无附件 + 默认模型 + default 权限(基础路径)
  • P1 常用用例(7 个):单文件、单文件夹、多文件、多文件夹、切换模型、切换权限
  • P2 边界用例(3-5 个):超大文件、并发对话、协议错误、进程崩溃

推荐用例清单(正交设计):

#文件夹关联文件上传模型权限对话中切换优先级
1默认default-P0
2关联默认default-P1
3上传默认default-P1
4关联上传默认default-P1
5默认auto_edit-P1
6默认yolo-P1
7默认default切换模型P1
8默认default切换权限P1
9上传(超大)默认default-P2
10默认default并发对话P2
11默认defaultbinary 崩溃P2

3. Binary 前置条件

3.1 Binary 路径解析

路径解析策略二选一,推荐选项 2:

选项 1 - Hardcode 路径(不推荐,脆弱):

const AIONRS_BINARY_PATH = '/Users/zhoukai/.local/bin/aionrs';

选项 2 - 从 PATH 查找(推荐,更健壮)

// 通过 binaryResolver 查找 const binary = await ipcBridge.fs.findAionrsBinary.invoke(); if (!binary) { test.skip('aionrs binary not found in PATH or ~/.local/bin/aionrs, skipping E2E tests'); }

解析顺序(binaryResolver):环境变量AION_CLI_PATH~/.aionui/bin/aion-<platform>-<arch>→ 系统 PATH 中的aion命令。

E2E 实现规范:

// tests/e2e/setup/aionrs.setup.ts export async function checkAionrsBinary(page: Page): Promise<boolean> { try { const binary = await invokeBridge(page, 'fs.findAionrsBinary'); if (!binary) { console.error('[E2E Setup] aionrs binary not found in PATH or ~/.local/bin/aionrs'); return false; } console.log(`[E2E Setup] aionrs binary found: ${binary}`); return true; } catch (error) { console.error('[E2E Setup] Failed to check aionrs binary:', error); return false; } } // tests/e2e/specs/chat-aionrs/*.spec.ts test.beforeAll(async ({ page }) => { const hasBinary = await checkAionrsBinary(page); if (!hasBinary) { test.skip('aionrs binary not found, skipping E2E tests'); } });

关键要求:若 binary 不存在,必须test.skip()并打印明确错误信息(不要悄悄跳过)。

3.2 Binary 启动与超时

启动流程:

  1. 创建 aionrs agent 实例
  2. 调用spawn(binaryPath, args, { env, stdio: ['pipe', 'pipe', 'pipe'] })
  3. 等待ready事件(JSON Lines:{"type":"ready","session_id":"...","capabilities":{...}}
  4. 超时时间:30s

E2E timeout 设置:

test( 'should start aionrs conversation', async ({ page }) => { // Playwright test timeout: 60s(留足 binary 启动时间) }, { timeout: 60000 } );

失败场景:

  • 启动超时:抛出Error('aionrs ready timeout (30s)')
  • 进程崩溃:childProcess.on('exit')触发,前端收到error事件

4. 临时目录与清理契约

4.1 临时目录规范

/tmp/e2e-chat-aionrs-<scenario>-<timestamp>/ ├── test-file.txt # 文件上传测试文件 ├── test-folder/ # 文件夹关联测试目录 │ └── sample.md └── .aionrs/ # aionrs session 文件(binary 自动创建)

命名规范:<scenario>为用例场景描述(如no-attachsingle-filesingle-foldermulti-attach);<timestamp>Date.now()YYYYMMDD-HHmmss。创建时机:beforeEach()或用例开始前。

4.2 清理契约(必须按序执行,避免外键冲突)

afterEach(async ({ page }) => { const conversationId = /* 当前用例的对话 ID */; const tmpDir = /* 当前用例的临时目录 */; try { // 1. 停止 aionrs binary 进程 await invokeBridge(page, 'conversation.stop', { conversation_id: conversationId }); // 2. 清理 DB(级联删除 messages) await invokeBridge(page, 'db.exec', { sql: "DELETE FROM conversations WHERE name LIKE 'E2E-aionrs-%'" }); // 3. 清理 FS(临时目录 + aionrs session 文件) await invokeBridge(page, 'fs.rm', { path: tmpDir, recursive: true }); // 4. 清理 UI state(ESC×5 关闭所有弹窗/模态框) for (let i = 0; i < 5; i++) { await page.keyboard.press('Escape'); await page.waitForTimeout(100); } // 5. 清理 sessionStorage await page.evaluate(() => { sessionStorage.clear(); }); // 6. 验证清理完成(可选,但推荐) const remaining = await invokeBridge(page, 'db.query', { sql: "SELECT COUNT(*) as count FROM conversations WHERE name LIKE 'E2E-aionrs-%'" }); if (remaining[0].count > 0) { throw new Error(`E2E cleanup failed: ${remaining[0].count} conversations still exist`); } } catch (error) { // 清理失败必须 throw(team-lead 硬性要求) console.error('[E2E Cleanup] Failed:', error); throw error; } });

清理范围总表

资源类型清理规则验证方式
DB conversationsDELETE WHERE name LIKE 'E2E-aionrs-%'SELECT COUNT(*)期望 0
DB messages级联删除(ON DELETE CASCADE自动清理
FS 临时目录rm -rf /tmp/e2e-chat-aionrs-*fs.existsSync()期望 false
FS aionrs session包含在临时目录内同上
UI stateESC×5 + 导航到安全页面(如/guid截图验证
sessionStorageclear()sessionStorage.length === 0

对话命名规范(清理 SQL 依赖此前缀):

const conversationName = `E2E-aionrs-${scenario}-${Date.now()}`; // 示例: 'E2E-aionrs-no-attach-1745327890123'

关键要求:前缀必须E2E-aionrs-;包含场景描述(便于日志追溯);包含时间戳(避免重名)。

5. 数据库验证字段

5.1 conversations 表验证

字段类型验证规则
idTEXT PK非空,UUID 格式
nameTEXT匹配'E2E-aionrs-*'模式
typeTEXT固定'aionrs'
modelTEXT模型 ID(如'claude-opus-4-7'
statusTEXT'pending' \| 'running' \| 'finished'
extraTEXT (JSON)见 extra 字段结构
created_atINTEGER时间戳(ms)
updated_atINTEGER≥ created_at
extra 字段结构(JSON)
{ "workspace": "/tmp/e2e-chat-aionrs-...", "sessionMode": "default" | "auto_edit" | "yolo", "lastTokenUsage": { "totalTokens": 1234 }, "model": { "id": "anthropic", "useModel": "claude-opus-4-7", "name": "Anthropic", "baseUrl": "https://api.anthropic.com/v1", "platform": "claude", "...": "..." } }

对应关系:sessionModesaveSessionMode()持久化;lastTokenUsagesaveContextUsage()持久化;model由 guid 页模型选择 hook 持久化。

5.2 messages 表验证

字段类型验证规则
idTEXT PK非空,UUID 格式
conversation_idTEXT FK关联 conversations.id
msg_idTEXTbinary 流式 msg_id(AI 回复)或用户消息 ID
typeTEXT'text' \| 'tool_group' \| 'thinking' \| ...
contentTEXT (JSON)见 content 字段结构
positionTEXT'left' \| 'right' \| 'center' \| 'pop'
statusTEXT'finish' \| 'pending' \| 'error' \| 'work'
created_atINTEGER时间戳(ms)
content 字段结构(JSON)

用户消息(position='right')

{ "content": "用户输入的消息文本", "attachedFiles": ["/tmp/.../test.txt"], "attachedDirs": ["/tmp/.../test-folder"] }

AI 文本回复(type='text', position='left')

{ "content": "AI 回复的文本内容(增量拼接)" }

思考消息(type='thinking')

{ "content": "<think> 标签或 thought 事件内容", "duration": 1234, "status": "thinking" | "done" }

工具调用(type='tool_group')

[ { "callId": "tool_call_uuid", "name": "edit_file", "description": "Edit /tmp/.../test.txt", "status": "Confirming" | "Executing" | "Success" | "Error" | "Canceled", "resultDisplay": "...", "renderOutputAsMarkdown": false } ]

5.3 E2E 断言示例

test('should verify DB records after conversation', async ({ page }) => { const conversationId = /* ... */; // 1. 验证 conversation 存在且类型正确 const conv = await invokeBridge(page, 'conversation.get', { id: conversationId }); expect(conv).toBeDefined(); expect(conv.type).toBe('aionrs'); expect(conv.status).toBe('finished'); expect(conv.extra.sessionMode).toBe('default'); // 2. 验证至少有 2 条消息(用户 + AI) const messages = await invokeBridge(page, 'db.query', { sql: 'SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC', params: [conversationId] }); expect(messages.length).toBeGreaterThanOrEqual(2); // 3. 验证用户消息 const userMsg = messages[0]; expect(userMsg.position).toBe('right'); expect(userMsg.type).toBe('text'); expect(JSON.parse(userMsg.content).content).toContain('Hello'); // 4. 验证 AI 回复 const aiMsg = messages.find(m => m.position === 'left' && m.type === 'text'); expect(aiMsg).toBeDefined(); expect(aiMsg.status).toBe('finish'); expect(JSON.parse(aiMsg.content).content).toBeTruthy(); });

6. 可测试性评估与>// AionrsSendBox.tsx <div>// 发送按钮 <Button>test('should complete aionrs conversation with no attachments', async ({ page }) => { // 1. 导航到 guid 页 await page.goto('/#/guid'); // 2. 选择 aionrs agent await page.click('[data-agent-backend="aionrs"][data-agent-selected="false"]'); // 3. 输入消息(通过 Playwright locator) const textarea = page.locator('textarea[placeholder*="aionrs"]'); await textarea.fill('Hello, aionrs!'); // 4. 点击发送按钮 await page.click('.send-button-custom'); // 或 [data-testid="aionrs-send-btn"] // 5. 等待导航到对话页 await page.waitForURL(/\/conversation\/aionrs\/.+/, { timeout: 10000 }); // 6. 提取 conversationId const url = page.url(); const conversationId = url.match(/\/conversation\/aionrs\/(.+)/)?.[1]; expect(conversationId).toBeTruthy(); // 7. 等待 AI 回复(轮询 DB) await waitForAIResponse(page, conversationId, { timeout: 60000 }); // 8. 验证 DB 记录 const messages = await invokeBridge(page, 'db.query', { sql: 'SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at', params: [conversationId], }); expect(messages.length).toBeGreaterThanOrEqual(2); expect(messages[0].position).toBe('right'); // 用户消息 expect(messages[1].position).toBe('left'); // AI 回复 });

7. 边界与异常场景

7.1 Binary 层异常

场景预期行为
binary 不存在test.skip()+ 明确错误信息
启动超时(30s)抛出Error('aionrs ready timeout')
进程崩溃前端收到error事件,对话标记为 error
resume 失败自动降级为新 session

7.2 并发对话

同时打开 2 个 aionrs 对话,轮流发送消息。预期每个对话独立维护 binary 进程 + session(进程管理实例独立)。验证点:DB 中有 2 条conversations记录(不同id);每个对话有各自的sessionId(从 binaryready事件获取)。

7.3 超大文件上传

上传 >100MB 文件(或 >1000 个文件)。预期前端文件处理层(FileService.processDroppedFiles())限制大小/数量并显示错误提示。验证点:E2E 创建 100MB 测试文件尝试上传,验证是否显示错误提示。

7.4 协议错误

Binary 返回非法 JSON Lines(如{"type":"unknown"})。预期前端解析失败、记录错误日志、不崩溃。对应的处理模式是进程端用try-catch包裹JSON.parse()

8. 待决策议题与决策结果

需求文档记录了四个待决策议题及其决策状态,其中三个已有明确结论:

议题内容决策
议题 0(新增)E2E 环境 provider 配置前置条件至少 1 个非 Google Auth provider 且至少有 2 个可用 model;不满足则降级或 skip
议题 1模型切换是否需重启 binary?(P0)E2E 探测式测试记录当前行为,只验证 DB 字段更新
议题 2权限 "always allow" 是否需持久化?(P2)B:保持内存存储(AionrsApprovalStore),持久化属产品需求
议题 4CI 环境 binary 来源?(P0)C:短期 skip,长期 DevOps 配置

议题 0 动态 model 选择实现

// tests/e2e/setup/aionrs.setup.ts export async function getAionrsTestModels(page: Page): Promise<{ defaultModel: { providerId: string; modelId: string } | null; switchModel: { providerId: string; modelId: string } | null; }> { const providers = await invokeBridge(page, 'mode.getModelConfig'); // 过滤 gemini-with-google-auth const availableProviders = providers.filter( (p) => !p.platform?.toLowerCase().includes('gemini-with-google-auth') && p.enabled !== false ); if (availableProviders.length === 0) return { defaultModel: null, switchModel: null }; const firstProvider = availableProviders[0]; const models = firstProvider.model || []; return { defaultModel: models.length > 0 ? { providerId: firstProvider.id, modelId: models[0] } : null, switchModel: models.length > 1 ? { providerId: firstProvider.id, modelId: models[1] } : null, }; }

降级策略:若只有 1 个 model,测试降级为"只验证当前 model,跳过切换场景";若无可用 provider,test.skip('No available providers for aionrs, skipping E2E tests')

议题 1 模型切换探测测试

test('模型切换探测', async ({ page }) => { // 1. 发送消息 A(模型 M1) // 2. 切换模型到 M2 // 3. 发送消息 B // 4. 查 DB:messages[B].extra.model === M2 ? '运行时切换生效' : '需重启' });

9. 交付文档清单与后续路线

Gate 流程产出的文档清单:

文档路径状态
Gate 1 需求文档requirements.zh.md✅ 完成
Gate 1 讨论记录discussion-log.zh.md✅ 完成(双 reviewer 审核)
Gate 2 测试用例test-cases.zh.md✅ 已有初稿(TC-A-01 ~ TC-A-15,P0/P1/P2 分级)
Gate 3 实现映射implementation-mapping.zh.md待完善

下一步路线(Gate 1 → Gate 3):

  1. ✅ Gate 1 完成:analyst 起草需求 + designer/engineer 审核 + team-lead 决策
  2. ⏳ Gate 2:designer 起草 test-cases(15 个用例,P0/P1/P2 分级)
  3. ⏳ 前置工作(阻塞 Gate 3):engineer 补充对话页 contenteditable="false">【免费下载链接】AionUiOpen-source 24/7 Cowork app for OpenClaw, Hermes, Claude Code, Codex, OpenCode and 20+ more CLI Agent | Customize your assistants | Team them up|Star if you like it!项目地址: https://gitcode.com/GitHub_Trending/ai/AionUi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 8:51:35

AI应用开发必备Python基础:从环境到实战的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:50:38

STM32H503单通道ADC电压采集实战:时钟、校准与滤波

简介&#xff1a;面向STM32H系列单片机开发者&#xff0c;提供基于HAL库的单通道ADC电压采集驱动&#xff0c;解决从寄存器配置到数据读取的快速落地问题。资源包含完整可编译的Keil工程&#xff0c;共268个文件&#xff0c;其中141个头文件与121个C源文件构成核心代码&#xf…

作者头像 李华
网站建设 2026/9/10 8:50:01

Context-Mode:基于SQLite+FTS5+BM25的轻量级上下文裁剪机制

1. 项目概述&#xff1a;Context-Mode 不是玄学&#xff0c;而是现代智能体系统里最务实的上下文调度机制 “Context-mode”这个词最近在开发者社区里频繁出现&#xff0c;尤其和 MCP、SQLite、FTS5、BM25 这几个词绑在一起刷屏。很多人第一反应是——这又是个新造的概念&…

作者头像 李华
网站建设 2026/9/10 8:49:48

东华OJ第64题N的倍数C++解法:循环取模与格式避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 8:49:29

Codex不是模型而是协议:Agent时代的任务执行标准

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华