chore: add reference docs, scripts, tests, demo, prototypes
- 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>
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
# 任务资产与日志 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.`
|
||||
|
||||
结果:
|
||||
|
||||
```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/<id>/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/<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`
|
||||
|
||||
结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
结果仍然是:
|
||||
|
||||
```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](<D:/My Coding Project/KSP_Project/app/routes/web.py:1080>) 中 `_parse_state_node_rows()` 用 `enumerate(..., start=1)`,但读取 `node_locations[index]` / `node_state_labels[index]`。表单数组是 0-based,第一个 node 应该用 `row_index`,当前第一个 node 永远读不到这些扩展字段。
|
||||
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:1130>) 把 target location 存进 row key `"location"`。
|
||||
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:1157>) `_sync_state_nodes()` 却读取 `row.get("target_location")`。即使修掉 index,target location 仍会因为 key 不一致继续丢失。
|
||||
|
||||
### P0-2: 资产详情页快速新增日志 JSON 路由忽略 location/state/node 扩展字段
|
||||
|
||||
资产详情页底部 quick modal 的 `saveLog()` 走 JSON:
|
||||
|
||||
- `/assets/<id>/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](<D:/My Coding Project/KSP_Project/app/routes/web.py:3917>) JSON create path 手写字段映射,没有复用 `_apply_asset_entry_fields()` / `_parse_state_node_rows()`。
|
||||
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:3989>) JSON edit path 也有同样问题。
|
||||
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:252>) 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](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:229>) `openNewLog('event')` 会隐藏 `log-end-wrap`。
|
||||
- `log-end-wrap` 里同时包含 Start At 和 End At。
|
||||
- 用户新建 Event Point 时没有地方填写 Start At。
|
||||
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:261>) `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](<D:/My Coding Project/KSP_Project/app/templates/asset_entry_form.html:320>)
|
||||
|
||||
状态:未完整浏览器复现,但静态检查明确有风险。
|
||||
|
||||
### P1-2: 资产详情页日志搜索和类型筛选控件没有实际逻辑
|
||||
|
||||
资产详情页有:
|
||||
|
||||
- `#ad-lq`
|
||||
- `#ad-lf`
|
||||
|
||||
但模板中没有对应筛选脚本,也没有表单提交参数。控件看起来可用,实际不会过滤日志表。
|
||||
|
||||
位置:
|
||||
|
||||
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:74>)
|
||||
|
||||
状态:静态检查明确未实现。
|
||||
|
||||
## 当前通过项
|
||||
|
||||
- 新建资产:通过。
|
||||
- 修改资产本体字段:通过。
|
||||
- 删除资产:通过,关联日志随资产清理。
|
||||
- 正式日志表单的 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_<timestamp>`。
|
||||
- 固定 `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/<id>/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 条。
|
||||
Reference in New Issue
Block a user