# 任务资产与日志 CRUD 复测报告 日期:2026-05-31 ## 结论 本轮只做测试和报告,没有修改业务代码。 资产本体的新增、修改、删除主流程基本可用:测试资产可以创建,可以通过表单路由修改 `name/program/home_region/note`,也可以删除,删除后数据库中不再存在该测试资产。 资产日志和 state node 仍有 P0 问题。正式日志表单创建/编辑 state interval 时,entry 本体字段能保存,但 state node 的 `state_label`、`previous_location`、`transit_location`、`target_location` 全部没有保存。资产详情页的快速新增日志 modal 走 JSON 路由,这条路径更不完整:state/location/node location 字段被忽略;快速新增 Event Point 还会因为没有 start time 直接 400。 ## 测试范围 按用户要求覆盖: - 新建资产 - 修改资产信息 - 删除资产 - 新建资产日志 - 修改资产日志 - 修改 state node 信息,重点包括 target location 测试数据使用 `TEST_Codex_...` 前缀,测试结束后已清理。复测后查询确认剩余 `TEST_Codex_%` 资产数量为 0。 ## 执行记录 ### 1. 新建资产 通过 `/assets/new?sim_time=2060-03-12T09:00` UI 表单创建: - Name: `TEST_Codex_Asset_20260531_719359` - Asset Type: `Station` - Program: `TEST Program Alpha` - Home Region: `LEO` - Note: `Created by Codex asset CRUD retest; safe to delete.` 结果: - POST 后 302 跳转到资产详情页。 - 详情页 H1 显示测试资产名。 - DB 中存在该资产。 状态:通过。 ### 2. 修改资产信息 通过 `/assets//edit` 表单路由提交: - Name 改为 `TEST_Codex_Asset_20260531_719359_EDITED` - Program 改为 `TEST Program Beta` - Home Region 改为 `Mars Orbit` - Note 改为 `Edited by Codex asset CRUD retest.` 结果: ```json { "status": 302, "name": "TEST_Codex_Asset_20260531_719359_EDITED", "program": "TEST Program Beta", "home_region": "Mars Orbit", "note": "Edited by Codex asset CRUD retest." } ``` 状态:通过。 备注:使用 in-app browser 直接填 edit form 时遇到浏览器控制层 CDP/clipboard 超时,我没有把它记为应用 bug;最终用同一个 HTTP 表单路由验证保存契约。 ### 3. 删除资产 通过 `/assets//delete` 表单路由删除测试资产。 结果: ```json { "status": 302, "location": "/assets?sim_time=2060-03-12T09:00", "exists_after_delete": false } ``` 状态:通过。 ## 日志与 State Node 问题 ### P0-1: 正式日志表单没有保存 state node 的 location/state 字段 操作: 通过 `/assets//entries/new` 的正式表单路由创建 state interval: - Entry title: `TEST Formal State 20260531_719359` - Mission: `TEST Mission Beta` - Start/End: `2060-03-13T09:00` 到 `2060-03-16T09:00` - Node title: `Formal Node A` - Node state: `Docked` - Previous Location: `LEO` - Transit Location: `Transfer` - Target Location: `Mars Orbit` 结果: ```json { "title": "Formal Node A", "detail": "Formal node A detail", "state_label": null, "previous_location": null, "transit_location": null, "target_location": null } ``` 再通过 `/assets//entries//edit` 修改同一个 node: - Node title 改为 `Formal Node A EDITED` - Node state 改为 `Exploration` - Previous Location 改为 `Mars Orbit` - Transit Location 改为 `Transfer` - Target Location 改为 `Mars Surface` 结果仍然是: ```json { "title": "Formal Node A EDITED", "detail": "Formal node A detail edited", "state_label": null, "previous_location": null, "transit_location": null, "target_location": null } ``` 状态:未通过。 根因线索: - [app/routes/web.py]() 中 `_parse_state_node_rows()` 用 `enumerate(..., start=1)`,但读取 `node_locations[index]` / `node_state_labels[index]`。表单数组是 0-based,第一个 node 应该用 `row_index`,当前第一个 node 永远读不到这些扩展字段。 - [app/routes/web.py]() 把 target location 存进 row key `"location"`。 - [app/routes/web.py]() `_sync_state_nodes()` 却读取 `row.get("target_location")`。即使修掉 index,target location 仍会因为 key 不一致继续丢失。 ### P0-2: 资产详情页快速新增日志 JSON 路由忽略 location/state/node 扩展字段 资产详情页底部 quick modal 的 `saveLog()` 走 JSON: - `/assets//entries/new` - `Content-Type: application/json` 这条后端路径只写: - `entry_kind` - `title` - `summary` - `start_at` - `end_at` - state node 的 `title/detail/at_time` 它没有写入: - entry 的 `mission_label` - node 的 `state_label` - node 的 `previous_location` - node 的 `transit_location` - node 的 `target_location` 复测 payload 中传了这些字段: ```json { "state_label": "Maintenance", "location": "Lunar Orbit", "state_nodes": [ { "state_label": "Maintenance", "previous_location": "LEO", "transit_location": "Transfer", "target_location": "Lunar Orbit" } ] } ``` 保存后 DB 中 node 仍是: ```json { "state_label": null, "previous_location": null, "transit_location": null, "target_location": null } ``` 状态:未通过。 根因线索: - [app/routes/web.py]() JSON create path 手写字段映射,没有复用 `_apply_asset_entry_fields()` / `_parse_state_node_rows()`。 - [app/routes/web.py]() JSON edit path 也有同样问题。 - [app/templates/asset_detail.html]() quick modal 的 `state_nodes` payload 只包含 `title/at_time/detail`,UI 本身也没有 target location/state node fields。 ### P0-3: 资产详情页快速新增 Event Point 无法保存 操作: 模拟资产详情页 `openNewLog('event')` 后 `saveLog()` 的 JSON 行为:`start_at=null`,`end_at=null`。 结果: - HTTP 400 - 数据库报错:`null value in column "start_at" of relation "asset_log_entries" violates not-null constraint` 原因: - [app/templates/asset_detail.html]() `openNewLog('event')` 会隐藏 `log-end-wrap`。 - `log-end-wrap` 里同时包含 Start At 和 End At。 - 用户新建 Event Point 时没有地方填写 Start At。 - [app/templates/asset_detail.html]() `saveLog()` 仍把 `start_at` 作为可空值发给后端。 状态:未通过。 ### P1-1: 正式日志编辑页切换 Entry Type 的 JS 有潜在错误 代码中: ```js document.getElementById("state-wrap").style.display = isState ? "" : "none"; ``` 但当前模板没有 `id="state-wrap"` 的元素。切换 entry type 时会触发空引用错误。 位置: - [app/templates/asset_entry_form.html]() 状态:未完整浏览器复现,但静态检查明确有风险。 ### P1-2: 资产详情页日志搜索和类型筛选控件没有实际逻辑 资产详情页有: - `#ad-lq` - `#ad-lf` 但模板中没有对应筛选脚本,也没有表单提交参数。控件看起来可用,实际不会过滤日志表。 位置: - [app/templates/asset_detail.html]() 状态:静态检查明确未实现。 ## 当前通过项 - 新建资产:通过。 - 修改资产本体字段:通过。 - 删除资产:通过,关联日志随资产清理。 - 正式日志表单的 entry 本体字段保存:部分通过,`title/mission_label/start_at/end_at/summary/note` 可保存和修改。 ## 当前未通过项 - 正式日志表单保存 state node 的 `target_location`:失败。 - 正式日志表单保存 state node 的 `state_label/previous_location/transit_location`:失败。 - 资产详情页 quick modal 保存 state node 扩展字段:失败。 - 资产详情页 quick modal 新增 Event Point:失败。 - 资产详情页日志搜索/类型筛选:未实现。 - 正式日志编辑页切换 Entry Type:存在 JS 空引用风险。 ## 改进建议 ### P0 1. 统一日志保存路径。 - 不要让 JSON create/edit 和 form create/edit 各写一套字段映射。 - 建议把 request JSON 转成同一份 internal DTO,然后复用同一个 validation + persistence 函数。 2. 修复 `_parse_state_node_rows()` 的数组下标。 - 读取 node 扩展字段时使用 `row_index`,不要用从 1 开始的 `index`。 3. 修复 target location key。 - `_parse_state_node_rows()` 应输出 `"target_location"`,或 `_sync_state_nodes()` 应读取 `"location"`;两边必须一致。 4. 资产详情 quick modal 要么删除,要么补齐。 - 如果保留 quick modal,state log 必须能填写 node state/previous/transit/target。 - Event Point 必须显示 Start At。 - JSON route 必须保存同等字段。 5. 后端 JSON 错误不要向用户暴露 SQLAlchemy/psycopg 全量错误。 - 当前 Event Point 失败时返回了完整 SQL、参数和 DB 约束信息。 - 应返回用户级 validation error,例如 `Start At is required`。 ### P1 1. 修复 `state-wrap` 空引用。 2. 实现资产详情日志搜索/类型筛选,或移除未接线控件。 3. 资产详情日志表应展示 node 的 state/location 字段,否则保存成功与否很难肉眼验证。 4. 给 location/state combobox 增加可测试的稳定选择器或 data attributes。 ## 端到端测试方向 建议新增一个独立的资产 CRUD E2E 套件,必须使用唯一测试前缀并自动清理。 ### Fixture - 创建 `TEST_E2E_ASSET_`。 - 固定 `sim_time=2060-03-12T09:00`。 - 每个测试结束查询并删除 `TEST_E2E_%` 资产。 ### 用例 1: 资产本体 CRUD 1. 打开 `/assets/new?sim_time=2060-03-12T09:00`。 2. 填写 name/type/program/home_region/note。 3. 提交后断言跳到详情页。 4. 打开 edit 页修改 name/program/home_region/note/retired。 5. 提交后断言详情页 Quick Info 和 DB 一致。 6. 删除资产。 7. 断言列表页搜不到,DB 不存在。 ### 用例 2: 正式 State Interval + State Node 1. 从资产详情点击 `+Entry` 或打开 `/assets//entries/new?kind=state`。 2. 填写 entry title、mission、start/end、summary。 3. 添加两个 state nodes。 4. 每个 node 填 title/time/detail/state/previous/transit/target。 5. 保存后断言 DB 中两个 node 全字段一致,并按 `at_time` 排序。 6. 打开 edit 页修改第一个 node 的 target location。 7. 保存后断言 DB 更新。 8. 详情页断言 current location / log row location 使用最新 target location。 ### 用例 3: Quick Modal State Log 1. 在资产详情点击 `+ State Interval`。 2. 验证 modal 是否有足够字段保存 state node location/state。 3. 保存后断言 JSON route 与正式 form route 行为一致。 ### 用例 4: Quick Modal Event Point 1. 在资产详情点击 `+ Event Point`。 2. 断言 Start At 可见且必填。 3. 不填 Start At 时应显示 validation,不应返回原始 SQL 错误。 4. 填 Start At 后保存成功。 ### 用例 5: 日志编辑和删除 1. 打开 entry edit 页。 2. 修改 entry 本体字段。 3. 修改已有 node。 4. 新增一个 node。 5. 删除一个 node。 6. 保存后断言 DB 与页面都一致。 7. 删除 entry 后断言关联 state nodes 级联删除。 ### 用例 6: 资产详情日志筛选 1. 给同一 asset 创建 state 和 event 两类日志。 2. 输入搜索词,断言行数变化。 3. 选择 `State Interval`,断言只显示 state。 4. 选择 `Event Point`,断言只显示 event。 ## 复测清理状态 - `TEST_Codex_Asset_20260531_719359` / edited variant:已删除。 - `TEST_Codex_EventModal_20260531_719359`:已删除。 - DB 查询 `TEST_Codex_%`:0 条。