fix(api+frontend): codex review 六件——死因空白strip/终态422防500/落盘回读断言/validator精确断言/promoted确认路径/文案不越权承诺 [vps]
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
- Consumes: 无(最底层)。
|
||||
- Produces: `upsert_factor(registry, name, hypothesis, status="incubating", origin=None, description=None) -> dict`——description 仅在**建条**时写入条目(与 hypothesis/origin 同级);同名幂等返回**不覆盖** description(同 origin 纪律,改描述走升版本)。后续 Task 3 调用 `vr.upsert_factor(..., description=cand["description"])`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(追加到 `tests/factor/test_version_registry.py`)
|
||||
- [x] **Step 1: 写失败测试**(追加到 `tests/factor/test_version_registry.py`)
|
||||
|
||||
```python
|
||||
def test_upsert_factor_description_built_once_not_overwritten():
|
||||
@@ -50,12 +50,12 @@ def test_upsert_factor_description_built_once_not_overwritten():
|
||||
assert "description" not in reg["factors"]["f2"]
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
- [x] **Step 2: 跑测确认失败**
|
||||
|
||||
Run: `python -m pytest tests/factor/test_version_registry.py::test_upsert_factor_description_built_once_not_overwritten -x -q`
|
||||
Expected: FAIL(`KeyError: 'description'` 或 assert 不等——现状无此参数)
|
||||
|
||||
- [ ] **Step 3: 最小实现**(改 `sanguo_factor/version_registry.py` upsert_factor)
|
||||
- [x] **Step 3: 最小实现**(改 `sanguo_factor/version_registry.py` upsert_factor)
|
||||
|
||||
```python
|
||||
def upsert_factor(registry: dict[str, Any], name: str, hypothesis: str,
|
||||
@@ -89,12 +89,12 @@ def upsert_factor(registry: dict[str, Any], name: str, hypothesis: str,
|
||||
return entry
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测确认通过**
|
||||
- [x] **Step 4: 跑测确认通过**
|
||||
|
||||
Run: `python -m pytest tests/factor/test_version_registry.py -x -q`
|
||||
Expected: PASS(既有测试不回归——不传 description 的旧调用零影响)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add sanguo_factor/version_registry.py tests/factor/test_version_registry.py
|
||||
@@ -113,7 +113,7 @@ git commit -m "feat(factor): upsert_factor 条目级 description——仅建条
|
||||
- Consumes: 无。
|
||||
- Produces: `normalize_candidates(raw) -> list[dict]`,每条 dict 为 `{"name": str, "expression": str, "description": str, "justification": str}`(description ≤120 字、空白压缩;**缺失 raise ValueError** 与 justification 同纪律,run_decompose 反馈循环自动重生成失败者)。Task 3 注册块消费 `cand["description"]`。
|
||||
|
||||
- [ ] **Step 1: 更新既有 fixture 并写失败测试**
|
||||
- [x] **Step 1: 更新既有 fixture 并写失败测试**
|
||||
|
||||
`tests/api/test_hypothesis_decompose.py` 三处改动:
|
||||
|
||||
@@ -146,12 +146,12 @@ def test_normalize_candidates_cleans_description():
|
||||
|
||||
(断言推导:压空白后= `"多空格 与换行 " + "长"*200` 共 6+200=206 字符,`[:120]` 截断= `"多空格 与换行 " + "长"*108`。)
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
- [x] **Step 2: 跑测确认失败**
|
||||
|
||||
Run: `python -m pytest tests/api/test_hypothesis_decompose.py -x -q`
|
||||
Expected: FAIL——`test_normalize_candidates_shape` 的 `assert out == [GOOD]` 通过(GOOD 已带 description 但 normalize 尚未透传该键,out 无 description 键不等);新用例 FAIL(无校验)。
|
||||
|
||||
- [ ] **Step 3: 最小实现**(`sanguo_api/hypothesis_decompose.py` 两处)
|
||||
- [x] **Step 3: 最小实现**(`sanguo_api/hypothesis_decompose.py` 两处)
|
||||
|
||||
prompt `_SYSTEM_TEMPLATE` 输出格式行与字段说明改为:
|
||||
|
||||
@@ -184,12 +184,12 @@ prompt `_SYSTEM_TEMPLATE` 输出格式行与字段说明改为:
|
||||
"description": desc, "justification": just})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测确认通过**
|
||||
- [x] **Step 4: 跑测确认通过**
|
||||
|
||||
Run: `python -m pytest tests/api/test_hypothesis_decompose.py -x -q`
|
||||
Expected: PASS 全绿。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add sanguo_api/hypothesis_decompose.py tests/api/test_hypothesis_decompose.py
|
||||
@@ -208,7 +208,7 @@ git commit -m "feat(api): 分解器输出 schema 四字段——description 必
|
||||
- Consumes: Task 1 `upsert_factor(..., description=...)`;Task 2 `cand["description"]`。
|
||||
- Produces: `GET /pipeline/factors` 每条 item 多 `"description": str`(缺省空串);`GET /pipeline/factors/{name}/detail` 的 `factor` 多 `"description": str`(缺省空串);decompose job `registered` 每条多 `"description": str`。Task 4/5 前端消费。
|
||||
|
||||
- [ ] **Step 1: 更新 mock 并写失败测试**
|
||||
- [x] **Step 1: 更新 mock 并写失败测试**
|
||||
|
||||
`tests/api/test_routes_pipeline_hypotheses.py` 三处 mock JSON 各加 description(示例):
|
||||
|
||||
@@ -252,12 +252,12 @@ def test_factors_and_detail_carry_description(client, tmp_path):
|
||||
|
||||
(`_seed_factor_card` 若只 seed 一个因子,others 为空列表断言恒真——保留作形状保险。)
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
- [x] **Step 2: 跑测确认失败**
|
||||
|
||||
Run: `python -m pytest tests/api/test_routes_pipeline_factors.py::test_factors_and_detail_carry_description tests/api/test_routes_pipeline_hypotheses.py -x -q`
|
||||
Expected: FAIL——KeyError `'description'`(端点未带出)/ job registered 无该键。注意:**hypotheses 测试此刻可能先因 normalize 缺 description 而 job failed**——那是 Task 2 已并入本 push 的正确链式失败,Step 3 修 routes 后一起绿。
|
||||
|
||||
- [ ] **Step 3: 最小实现**(`sanguo_api/routes_pipeline.py` 三处)
|
||||
- [x] **Step 3: 最小实现**(`sanguo_api/routes_pipeline.py` 三处)
|
||||
|
||||
注册块(约 1136 行):
|
||||
|
||||
@@ -290,12 +290,12 @@ factor_detail 返回 factor dict 加(`"hypothesis": hyp_id,` 行后):
|
||||
"description": e.get("description") or "",
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测确认通过**
|
||||
- [x] **Step 4: 跑测确认通过**
|
||||
|
||||
Run: `python -m pytest tests/api -x -q`
|
||||
Expected: PASS 全绿(含既有 job 契约/verdict/回写全家)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add sanguo_api/routes_pipeline.py tests/api/test_routes_pipeline_hypotheses.py tests/api/test_routes_pipeline_factors.py
|
||||
@@ -316,7 +316,7 @@ git commit -m "feat(api): 因子出生链落 description——注册块/factors/
|
||||
- Consumes: Task 3 端点返回的 `"description": str`(空串=无)。
|
||||
- Produces: `PipelineFactor.description?: string`、`FactorDetail.factor.description?: string`——Task 5 详情页消费后者。
|
||||
|
||||
- [ ] **Step 1: 类型与 mock 先行**(`pipeline.ts` 两接口各加一行)
|
||||
- [x] **Step 1: 类型与 mock 先行**(`pipeline.ts` 两接口各加一行)
|
||||
|
||||
```typescript
|
||||
origin?: string // D7 件②:decomposer/manual 身份戳
|
||||
@@ -339,7 +339,7 @@ git commit -m "feat(api): 因子出生链落 description——注册块/factors/
|
||||
|
||||
(至少给 mock 里 status 为 decaying 的 `fa_gross_margin` 与一条 decomposer 风格条目加非空 description,供断言。)
|
||||
|
||||
- [ ] **Step 2: 写失败测试**(`FactorFactory.spec.ts` 追加)
|
||||
- [x] **Step 2: 写失败测试**(`FactorFactory.spec.ts` 追加)
|
||||
|
||||
```typescript
|
||||
it('因子名下副行渲染 description(无则不渲染副行)', async () => {
|
||||
@@ -352,12 +352,12 @@ git commit -m "feat(api): 因子出生链落 description——注册块/factors/
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 跑测确认失败**
|
||||
- [x] **Step 3: 跑测确认失败**
|
||||
|
||||
Run: `cd frontend && npx vitest run src/views/pipeline/FactorFactory.spec.ts`
|
||||
Expected: FAIL(`.factor-cell` 不存在)。
|
||||
|
||||
- [ ] **Step 4: 实现看板副行**(`FactorFactory.vue` 因子列+样式)
|
||||
- [x] **Step 4: 实现看板副行**(`FactorFactory.vue` 因子列+样式)
|
||||
|
||||
```vue
|
||||
<el-table-column prop="id" label="因子" min-width="230">
|
||||
@@ -377,12 +377,12 @@ Expected: FAIL(`.factor-cell` 不存在)。
|
||||
.desc-line { font-size: 10.5px; color: var(--text-3); line-height: 1.4; }
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑测+类型检查**
|
||||
- [x] **Step 5: 跑测+类型检查**
|
||||
|
||||
Run: `cd frontend && npx vitest run src/views/pipeline/FactorFactory.spec.ts && npx vue-tsc --noEmit`
|
||||
Expected: PASS / 无输出。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/api/pipeline.ts frontend/src/mock/pipelineData.ts frontend/src/views/pipeline/FactorFactory.vue frontend/src/views/pipeline/FactorFactory.spec.ts
|
||||
@@ -407,7 +407,7 @@ git commit -m "feat(frontend): 工厂看板因子名副行展示出生 descripti
|
||||
- incubating/assessable/retired:点击→ElMessageBox.prompt 输入死因(validator 非空)→调 verdict→跳回工厂页。
|
||||
- promoted/decaying:第一段 ElMessageBox.confirm 重确认(文案:生产在用、合成层钉着版本,建议先走退役;确认继续判死)→第二段同上 prompt。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(`FactorDetail.spec.ts` 追加;该文件已有 mount 范式,mock getFactorDetail 返回值里加 description,并 mock postFactorVerdict)
|
||||
- [x] **Step 1: 写失败测试**(`FactorDetail.spec.ts` 追加;该文件已有 mount 范式,mock getFactorDetail 返回值里加 description,并 mock postFactorVerdict)
|
||||
|
||||
```typescript
|
||||
it('档案卡渲染因子自述行', async () => {
|
||||
@@ -427,12 +427,12 @@ git commit -m "feat(frontend): 工厂看板因子名副行展示出生 descripti
|
||||
|
||||
(`ElMessageBox.prompt` mock 掉弹窗;validator 逻辑在 prompt options 里,空值 reject——测试改为直接断言组件的 validator 函数或 mock prompt 走真实 validator;执行时以仓库该 spec 文件既有 mock 风格为准对齐,**不新造 mount 范式**。)
|
||||
|
||||
- [ ] **Step 2: 跑测确认失败**
|
||||
- [x] **Step 2: 跑测确认失败**
|
||||
|
||||
Run: `cd frontend && npx vitest run src/views/pipeline/FactorDetail.spec.ts`
|
||||
Expected: FAIL(无 .grave-btn / 无自述行)。
|
||||
|
||||
- [ ] **Step 3: 实现**(`FactorDetail.vue`)
|
||||
- [x] **Step 3: 实现**(`FactorDetail.vue`)
|
||||
|
||||
script 加:
|
||||
|
||||
@@ -489,12 +489,12 @@ IC 趋势卡后加危险区:
|
||||
</PanelCard>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测+类型检查**
|
||||
- [x] **Step 4: 跑测+类型检查**
|
||||
|
||||
Run: `cd frontend && npx vitest run src/views/pipeline/FactorDetail.spec.ts && npx vue-tsc --noEmit`
|
||||
Expected: PASS / 无输出。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/views/pipeline/FactorDetail.vue frontend/src/views/pipeline/FactorDetail.spec.ts
|
||||
@@ -505,17 +505,17 @@ git commit -m "feat(frontend): 因子详情页档案自述行+危险区送墓园
|
||||
|
||||
### Task 6: 全量回归 + push
|
||||
|
||||
- [ ] **Step 1: 后端 8 目录全量**
|
||||
- [x] **Step 1: 后端 8 目录全量**
|
||||
|
||||
Run: `python -m pytest tests/data_platform tests/portfolio tests/factor tests/api tests/trader tests/backtest tests/data tests/orchestrator -q`
|
||||
Expected: 全绿(基线 2780+,本计划新增约 4 用例)。
|
||||
|
||||
- [ ] **Step 2: 前端全量**
|
||||
- [x] **Step 2: 前端全量**
|
||||
|
||||
Run: `cd frontend && npx vitest run && npx vue-tsc --noEmit`
|
||||
Expected: 全绿(基线 77+,新增约 3 用例)。
|
||||
|
||||
- [ ] **Step 3: Push 触发 CI**
|
||||
- [x] **Step 3: Push 触发 CI**
|
||||
|
||||
```bash
|
||||
git push origin master
|
||||
@@ -523,6 +523,6 @@ git push origin master
|
||||
|
||||
Expected: Gitea Actions run(test→nas-deploy→nas-verify)三 job 绿;`[vps]` 标签逐条齐全(漏标=CI 红,先例 run 2866)。
|
||||
|
||||
- [ ] **Step 4: 汇报**
|
||||
- [x] **Step 4: 汇报**
|
||||
|
||||
向用户报:commit 清单+CI 状态+「待推 VPS」清单,等口令。**(完成后另起 codex review sub——不在本计划内,由主 session 编排。)**
|
||||
|
||||
@@ -76,10 +76,10 @@ describe('FactorDetail.vue 危险区送墓园(2026-10-09 补丁)', () => {
|
||||
vi.mocked(api.postFactorVerdict).mockResolvedValue({ ok: true, action: 'graveyard' })
|
||||
const w = await mountDetail()
|
||||
await w.find('.grave-btn').trigger('click')
|
||||
// validator:空死因拒/非空过
|
||||
// validator:空死因拒(返回提示串)/非空过(严格 true)——codex review HIGH-3 断言精确化
|
||||
const validator = captured.inputValidator as (v: string) => string | boolean
|
||||
expect(validator('')).toBeTruthy()
|
||||
expect(validator(' ')).toBeTruthy()
|
||||
expect(validator('')).toBe('死因必填')
|
||||
expect(validator(' ')).toBe('死因必填')
|
||||
expect(validator('死因')).toBe(true)
|
||||
expect(api.postFactorVerdict).toHaveBeenCalledWith(
|
||||
'insider_buy_decay60', 'graveyard', [], '概念与既有因子重复')
|
||||
@@ -103,4 +103,18 @@ describe('FactorDetail.vue 危险区送墓园(2026-10-09 补丁)', () => {
|
||||
expect(api.postFactorVerdict).not.toHaveBeenCalled()
|
||||
confirmSpy.mockRestore()
|
||||
})
|
||||
it('promoted 重确认通过后继续送墓园(确认路径,decaying 同款文案)', async () => {
|
||||
// codex review MEDIUM-3:补确认继续路径+生产态危险区提示
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue({} as never)
|
||||
vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: '衰减不止' } as never)
|
||||
vi.mocked(api.postFactorVerdict).mockResolvedValue({ ok: true, action: 'graveyard' })
|
||||
const w = await mountDetail({
|
||||
factor: { ...DETAIL.factor, status: 'decaying' },
|
||||
birth: null,
|
||||
})
|
||||
expect(w.text()).toContain('生产因子判死需二次确认')
|
||||
await w.find('.grave-btn').trigger('click')
|
||||
expect(api.postFactorVerdict).toHaveBeenCalledWith(
|
||||
'insider_buy_decay60', 'graveyard', [], '衰减不止')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,7 +56,8 @@ async function sendToGraveyard(): Promise<void> {
|
||||
burying.value = true
|
||||
try {
|
||||
await postFactorVerdict(name.value, 'graveyard', [], note)
|
||||
ElMessage.success('已入墓园(卡片已回写)')
|
||||
// codex review LOW-2:回写失败后端会降级留附记,不在此承诺「已回写」
|
||||
ElMessage.success('已入墓园')
|
||||
router.push('/pipeline/factors')
|
||||
} catch (e) {
|
||||
ElMessage.error(`判死失败: ${e instanceof Error ? e.message : e}`)
|
||||
|
||||
@@ -606,7 +606,7 @@ def factor_verdict(name: str, body: dict) -> dict:
|
||||
from sanguo_portfolio.strategy_registry import ensure_runtime_registry
|
||||
verdict = body.get("verdict")
|
||||
answers = body.get("answers") or []
|
||||
note = body.get("note") or ""
|
||||
note = str(body.get("note") or "").strip() # codex review:纯空白死因不可过(必填语义)
|
||||
if verdict not in ("promote", "revise", "graveyard"):
|
||||
raise HTTPException(422, "verdict 须为 promote/revise/graveyard")
|
||||
reg_path = ensure_runtime_registry(
|
||||
@@ -621,18 +621,22 @@ def factor_verdict(name: str, body: dict) -> dict:
|
||||
old = reg["factors"][name]["status"]
|
||||
extra: dict[str, Any] = {"answers": answers, "note": note,
|
||||
"reviewed_via": "console"}
|
||||
if verdict == "promote":
|
||||
extra["promoted_at"] = date.today().isoformat()
|
||||
entry = vr.transition(reg, name, "promoted", **extra)
|
||||
action = "promoted"
|
||||
elif verdict == "graveyard":
|
||||
if not note:
|
||||
raise HTTPException(422, "入墓园必须带 note(死因,可检索防重复造轮)")
|
||||
entry = vr.transition(reg, name, "graveyard", cause=note, **extra)
|
||||
action = "graveyard"
|
||||
else: # revise=留观:五问留痕,状态不动
|
||||
entry = reg["factors"][name]
|
||||
action = "revise(留观)"
|
||||
try:
|
||||
if verdict == "promote":
|
||||
extra["promoted_at"] = date.today().isoformat()
|
||||
entry = vr.transition(reg, name, "promoted", **extra)
|
||||
action = "promoted"
|
||||
elif verdict == "graveyard":
|
||||
if not note:
|
||||
raise HTTPException(422, "入墓园必须带 note(死因,可检索防重复造轮)")
|
||||
entry = vr.transition(reg, name, "graveyard", cause=note, **extra)
|
||||
action = "graveyard"
|
||||
else: # revise=留观:五问留痕,状态不动
|
||||
entry = reg["factors"][name]
|
||||
action = "revise(留观)"
|
||||
except ValueError as exc:
|
||||
# codex review:状态机拒绝(墓园终态重复判死/非法迁移)→422 中文原因,不再裸 500
|
||||
raise HTTPException(422, str(exc)) from exc
|
||||
vr.save_registry(reg_path, reg)
|
||||
vr.append_event(events_path, entry, old, action, **extra)
|
||||
if verdict == "graveyard":
|
||||
|
||||
@@ -69,11 +69,34 @@ def test_factors_and_detail_carry_description(client, tmp_path):
|
||||
with open(reg_path, encoding="utf-8") as f:
|
||||
reg = yaml.safe_load(f)
|
||||
reg["factors"][name]["description"] = "毛利变动因子自述"
|
||||
# codex review LOW-1:并排一个无 description 的存量形态因子,真测缺省=空串
|
||||
reg["factors"]["fa_legacy"] = {"name": "fa_legacy", "hypothesis": hyp,
|
||||
"status": "assessable", "versions": []}
|
||||
with open(reg_path, "w", encoding="utf-8") as f:
|
||||
yaml.safe_dump(reg, f, allow_unicode=True)
|
||||
items = client.get("/api/v1/pipeline/factors").json()["items"]
|
||||
assert next(i for i in items if i["id"] == name)["description"] == "毛利变动因子自述"
|
||||
assert next(i for i in items if i["id"] == "fa_legacy")["description"] == ""
|
||||
d = client.get(f"/api/v1/pipeline/factors/{name}/detail").json()
|
||||
assert d["factor"]["description"] == "毛利变动因子自述"
|
||||
# 存量无 description=空串非 null(前端免判空)
|
||||
assert all(isinstance(i.get("description", ""), str) for i in items)
|
||||
|
||||
|
||||
def test_graveyard_verdict_rejects_blank_note(client, tmp_path):
|
||||
"""codex review HIGH-1:纯空白死因不可过(必填语义,直连 API 绕过前端 trim)."""
|
||||
name, hyp = _seed_factor_card(client)
|
||||
r = client.post(f"/api/v1/pipeline/factors/{name}/verdict",
|
||||
json={"verdict": "graveyard", "note": " "})
|
||||
assert r.status_code == 422
|
||||
assert "note" in r.json()["detail"]
|
||||
|
||||
|
||||
def test_graveyard_repeat_returns_422_not_500(client, tmp_path):
|
||||
"""codex review MEDIUM-1:终态重复判死=422 中文原因,不裸 500."""
|
||||
name, hyp = _seed_factor_card(client)
|
||||
r1 = client.post(f"/api/v1/pipeline/factors/{name}/verdict",
|
||||
json={"verdict": "graveyard", "note": "重复提交防护"})
|
||||
assert r1.status_code == 200
|
||||
r2 = client.post(f"/api/v1/pipeline/factors/{name}/verdict",
|
||||
json={"verdict": "graveyard", "note": "再来一次"})
|
||||
assert r2.status_code == 422
|
||||
assert "墓园" in r2.json()["detail"]
|
||||
|
||||
@@ -274,6 +274,12 @@ class TestDecompose:
|
||||
assert job2["status"] == "completed"
|
||||
assert job2["registered"][0]["name"] == "llm_vol5"
|
||||
assert job2["registered"][0]["description"] == "5 日波动率因子自述"
|
||||
# codex review MEDIUM-2:回读 registry 验证 upsert_factor 真落盘(非仅回传)
|
||||
import os as _os
|
||||
import yaml as _yaml
|
||||
with open(_os.environ["SANGUO_FACTOR_REGISTRY"], encoding="utf-8") as f:
|
||||
reg_disk = _yaml.safe_load(f)
|
||||
assert reg_disk["factors"]["llm_vol5"]["description"] == "5 日波动率因子自述"
|
||||
items = client.get("/api/v1/pipeline/hypotheses").json()["items"]
|
||||
assert next(i for i in items if i["id"] == hyp)["state"] == "building"
|
||||
factors = _yaml.safe_load(reg.read_text())["factors"]
|
||||
|
||||
Reference in New Issue
Block a user