- Reference docs: known issues, user journey, location redesign, mission merge plan - Scripts: fill_location_state, merge_missions, migrate_location, migrate_state - Tests: e2e test suite, asset entries unit tests - Demo: location board, hifi prototypes, test screenshots - Data backup: asset log entries and state nodes - Update .gitignore Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 KiB
任务资产与日志 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/<id>/edit 表单路由提交:
- Name 改为
TEST_Codex_Asset_20260531_719359_EDITED - Program 改为
TEST Program Beta - Home Region 改为
Mars Orbit - Note 改为
Edited by Codex asset CRUD retest.
结果:
{
"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/<id>/delete 表单路由删除测试资产。
结果:
{
"status": 302,
"location": "/assets?sim_time=2060-03-12T09:00",
"exists_after_delete": false
}
状态:通过。
日志与 State Node 问题
P0-1: 正式日志表单没有保存 state node 的 location/state 字段
操作:
通过 /assets/<id>/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
结果:
{
"title": "Formal Node A",
"detail": "Formal node A detail",
"state_label": null,
"previous_location": null,
"transit_location": null,
"target_location": null
}
再通过 /assets/<id>/entries/<entry_id>/edit 修改同一个 node:
- Node title 改为
Formal Node A EDITED - Node state 改为
Exploration - Previous Location 改为
Mars Orbit - Transit Location 改为
Transfer - Target Location 改为
Mars Surface
结果仍然是:
{
"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/<id>/entries/newContent-Type: application/json
这条后端路径只写:
entry_kindtitlesummarystart_atend_at- state node 的
title/detail/at_time
它没有写入:
- entry 的
mission_label - node 的
state_label - node 的
previous_location - node 的
transit_location - node 的
target_location
复测 payload 中传了这些字段:
{
"state_label": "Maintenance",
"location": "Lunar Orbit",
"state_nodes": [
{
"state_label": "Maintenance",
"previous_location": "LEO",
"transit_location": "Transfer",
"target_location": "Lunar Orbit"
}
]
}
保存后 DB 中 node 仍是:
{
"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_nodespayload 只包含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 有潜在错误
代码中:
document.getElementById("state-wrap").style.display = isState ? "" : "none";
但当前模板没有 id="state-wrap" 的元素。切换 entry type 时会触发空引用错误。
位置:
状态:未完整浏览器复现,但静态检查明确有风险。
P1-2: 资产详情页日志搜索和类型筛选控件没有实际逻辑
资产详情页有:
#ad-lq#ad-lf
但模板中没有对应筛选脚本,也没有表单提交参数。控件看起来可用,实际不会过滤日志表。
位置:
状态:静态检查明确未实现。
当前通过项
- 新建资产:通过。
- 修改资产本体字段:通过。
- 删除资产:通过,关联日志随资产清理。
- 正式日志表单的 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
-
统一日志保存路径。
- 不要让 JSON create/edit 和 form create/edit 各写一套字段映射。
- 建议把 request JSON 转成同一份 internal DTO,然后复用同一个 validation + persistence 函数。
-
修复
_parse_state_node_rows()的数组下标。- 读取 node 扩展字段时使用
row_index,不要用从 1 开始的index。
- 读取 node 扩展字段时使用
-
修复 target location key。
_parse_state_node_rows()应输出"target_location",或_sync_state_nodes()应读取"location";两边必须一致。
-
资产详情 quick modal 要么删除,要么补齐。
- 如果保留 quick modal,state log 必须能填写 node state/previous/transit/target。
- Event Point 必须显示 Start At。
- JSON route 必须保存同等字段。
-
后端 JSON 错误不要向用户暴露 SQLAlchemy/psycopg 全量错误。
- 当前 Event Point 失败时返回了完整 SQL、参数和 DB 约束信息。
- 应返回用户级 validation error,例如
Start At is required。
P1
- 修复
state-wrap空引用。 - 实现资产详情日志搜索/类型筛选,或移除未接线控件。
- 资产详情日志表应展示 node 的 state/location 字段,否则保存成功与否很难肉眼验证。
- 给 location/state combobox 增加可测试的稳定选择器或 data attributes。
端到端测试方向
建议新增一个独立的资产 CRUD E2E 套件,必须使用唯一测试前缀并自动清理。
Fixture
- 创建
TEST_E2E_ASSET_<timestamp>。 - 固定
sim_time=2060-03-12T09:00。 - 每个测试结束查询并删除
TEST_E2E_%资产。
用例 1: 资产本体 CRUD
- 打开
/assets/new?sim_time=2060-03-12T09:00。 - 填写 name/type/program/home_region/note。
- 提交后断言跳到详情页。
- 打开 edit 页修改 name/program/home_region/note/retired。
- 提交后断言详情页 Quick Info 和 DB 一致。
- 删除资产。
- 断言列表页搜不到,DB 不存在。
用例 2: 正式 State Interval + State Node
- 从资产详情点击
+Entry或打开/assets/<id>/entries/new?kind=state。 - 填写 entry title、mission、start/end、summary。
- 添加两个 state nodes。
- 每个 node 填 title/time/detail/state/previous/transit/target。
- 保存后断言 DB 中两个 node 全字段一致,并按
at_time排序。 - 打开 edit 页修改第一个 node 的 target location。
- 保存后断言 DB 更新。
- 详情页断言 current location / log row location 使用最新 target location。
用例 3: Quick Modal State Log
- 在资产详情点击
+ State Interval。 - 验证 modal 是否有足够字段保存 state node location/state。
- 保存后断言 JSON route 与正式 form route 行为一致。
用例 4: Quick Modal Event Point
- 在资产详情点击
+ Event Point。 - 断言 Start At 可见且必填。
- 不填 Start At 时应显示 validation,不应返回原始 SQL 错误。
- 填 Start At 后保存成功。
用例 5: 日志编辑和删除
- 打开 entry edit 页。
- 修改 entry 本体字段。
- 修改已有 node。
- 新增一个 node。
- 删除一个 node。
- 保存后断言 DB 与页面都一致。
- 删除 entry 后断言关联 state nodes 级联删除。
用例 6: 资产详情日志筛选
- 给同一 asset 创建 state 和 event 两类日志。
- 输入搜索词,断言行数变化。
- 选择
State Interval,断言只显示 state。 - 选择
Event Point,断言只显示 event。
复测清理状态
TEST_Codex_Asset_20260531_719359/ edited variant:已删除。TEST_Codex_EventModal_20260531_719359:已删除。- DB 查询
TEST_Codex_%:0 条。