diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index c17d8e11..452829ea 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -6906,11 +6906,19 @@ func _smoke_save_slots() -> void: check(FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before, "sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕") - # ③ 新档原语(sv4):空槽创建=全零可玩白板,创建即激活并落盘 + # ③ 新档原语(sv4):非白板旧内容覆盖为全零可玩白板(判别锚:新档禁读旧档——变异可红), + # 创建即激活并落盘(生产路径只在空槽调用;本段预置旧内容是对原语白板保证的钉死,超生产面) + var fold := FileAccess.open(slot_tpl % 3, FileAccess.WRITE) + fold.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 999, + "adventure_total": 5, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}})) + fold.close() check(t.create_slot(3), "sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕") - check(t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint(), - "sv4 新档=全零可玩合法白板态(激活号 3/钥石 0/无断点)〔卡① sv4〕") - check(FileAccess.file_exists(slot_tpl % 3), "sv4 新档落盘(文件在位)〔卡① sv4〕") + check(t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint() + and t.adventure_total() == 0, + "sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕") + var created: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 3)) + check(int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0, + "sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕") # ④ 高版本拒读槽按槽成立+激活失败恢复切换前三态(sv9/sv10) var fhv := FileAccess.open(slot_tpl % 2, FileAccess.WRITE) diff --git a/game/tests/unit/core/save_boot_no_activate_test.gd b/game/tests/unit/core/save_boot_no_activate_test.gd index ea55e691..19afb2d4 100644 --- a/game/tests/unit/core/save_boot_no_activate_test.gd +++ b/game/tests/unit/core/save_boot_no_activate_test.gd @@ -1,17 +1,21 @@ # tests/unit/core/save_boot_no_activate_test.gd — 卡① sv2/sv3:启动态钉死类级契约 -# (无参实例=无激活态、内存=默认合法空态、无激活写盘一律拒绝并返回 false 且盘面零触碰、 -# 访问器 fail-safe 合法空值、越界槽号拒绝。真实 autoload boot 链断言在冒烟 V2-s1..s4 段—— -# --script 模式无 autoload,本套件为类级白盒面,双落点在实现报告实际对照表对账。) +# (无参实例=无激活态、内存=默认合法空态、无激活写盘一律拒绝并返回 false 且盘面零触碰 +# 〔含空路径派生的裸 ".tmp" 退化写目标——拆闸变异的可判别锚〕、MetaProgression.settle_adventure +# 生产链无激活触达写盘被拒、访问器 fail-safe 合法空值、越界槽号拒绝。 +# 真实 autoload boot 链断言在冒烟 V2-s1..s4 段——--script 模式无 autoload,本套件为类级 +# 白盒面+生产链对照,双落点在实现报告实际对照表对账。) # 载体:run_all 单元套件(显式实例化夹具——--script 模式无 autoload)。 extends SceneTree const SaveManagerScript := preload("res://core/save_manager.gd") +const MetaProgressionScript := preload("res://core/meta_progression.gd") const Kit := preload("res://tests/unit/test_kit.gd") func _init() -> void: _test_no_arg_instance_has_no_activation() _test_write_rejected_without_target() + _test_settle_chain_write_rejected() _test_out_of_range_slots_rejected() quit(Kit.finish("save_boot_no_activate")) @@ -33,6 +37,8 @@ func _test_no_arg_instance_has_no_activation() -> void: # ② 无激活态写盘一律拒绝并返回 false,盘面零触碰(sv3 类级——真实入口=冒烟 V2-s3 生产链) +# 判别锚:空 _path 的退化写目标=裸 ".tmp"(落 res://,拆闸实验实测杂散落点)—— +# 断言先清退化目标再核零新增,闸短路变异(拒绝口旁路、写盘真实发生)必红。 func _test_write_rejected_without_target() -> void: var sm := SaveManagerScript.new() var before := {} @@ -41,6 +47,10 @@ func _test_write_rejected_without_target() -> void: before[i] = [FileAccess.file_exists(p), FileAccess.get_file_as_bytes(p) if FileAccess.file_exists(p) else PackedByteArray(), FileAccess.get_modified_time(p) if FileAccess.file_exists(p) else 0] + var stray: Array[String] = ["res://.tmp", "res://.tmp.tmp"] # 空 _path 的 tmp 原子写派生名 + for sp in stray: + if FileAccess.file_exists(sp): + DirAccess.remove_absolute(sp) # 夹具清场——退化目标从零起 Kit.check_quiet(not sm.write_to_disk(), "sv3 无激活写盘拒绝(write_to_disk=false)〔卡① sv3〕") var unchanged := true @@ -54,11 +64,41 @@ func _test_write_rejected_without_target() -> void: unchanged = false Kit.check_quiet(unchanged, "sv3 拒写盘面零触碰(三槽存在性/字节/mtime 全等)〔卡① sv3〕") + var stray_clean := true + for sp in stray: + if FileAccess.file_exists(sp): + stray_clean = false + Kit.check_quiet(stray_clean, + "sv3 拒写退化写目标零触碰(裸 .tmp 零新增——拒绝口可判别锚)〔卡① sv3〕") # 拒读禁写态语义在位(高版本拒读→写拒绝——既有契约回归锚) sm._read_only = true Kit.check_quiet(not sm.write_to_disk(), "sv3 拒读禁写态写盘拒绝(既有契约保持)〔卡① sv3〕") +# ③ 生产链对照面(sv3 真实入口):MetaProgression.settle_adventure 无激活态触达写盘被拒 +# (生产结算链全段在位:mark_first_adventure_done→apply_adventure_summary→refresh_treasure_unlocks +# →write_to_disk;无激活态下落账内存、写盘拒绝、盘面零触碰——已知中间态的机检面) +func _test_settle_chain_write_rejected() -> void: + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + Kit.check_quiet(false, "sv3 链测试前置:数值表加载失败〔卡① sv3〕") + return + var sm := SaveManagerScript.new() + var meta := MetaProgressionScript.new(db, sm) + for sp in ["res://.tmp", "res://.tmp.tmp"]: + if FileAccess.file_exists(sp): + DirAccess.remove_absolute(sp) + Kit.check_quiet(not sm.has_write_target() and not sm.write_to_disk(), + "sv3 链前置:无激活态在位且直写被拒〔卡① sv3〕") + meta.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1, + "stopped_at": 2, "keystone_total": 0, "master_seed": 3}) + Kit.check_quiet(not sm.has_write_target() and sm.adventure_total() == 1, + "sv3 生产链:settle_adventure 落账内存(adventure_total=1)而写盘仍无目标(被拒不落盘)〔卡① sv3〕") + Kit.check_quiet(not FileAccess.file_exists("res://.tmp") + and not FileAccess.file_exists("res://.tmp.tmp"), + "sv3 生产链盘面:结算触达后退化写目标零新增(链上真实入口的盘面核验)〔卡① sv3〕") + + # ③ 越界槽号三原语拒绝+窥视越界返回不可读(原语边界 fail-safe) func _test_out_of_range_slots_rejected() -> void: var sm := SaveManagerScript.new() diff --git a/reports/iter33-实现报告-2026-10-21.md b/reports/iter33-实现报告-2026-10-21.md index 8f757216..3807ead6 100644 --- a/reports/iter33-实现报告-2026-10-21.md +++ b/reports/iter33-实现报告-2026-10-21.md @@ -7,7 +7,7 @@ ## 卡① 存档制数据层改造批(隐式单档 → 3 存档位)——已合并(见合并记录) -**状态**:T0–T4 完成;批末重闸 --code 全绿(回执见合并提交信息);独立验收审计通过(`reports/iter33-验收审计-2026-10-21.md` 卡① 节)。 +**状态**:T0–T4 完成;批末重闸 --code 全绿(回执见合并提交信息);独立验收审计两轮——首轮未过(sv3 拆闸实验判「拒写断言无判别力+生产链零对照面」、sv4 申报变异未执行且无判别力,两行缺漏回炉),回炉后断言强化(sv3 退化写目标锚+生产链对照面、sv4 旧内容判别锚)并补执行变异⑥⑦,复审通过(`reports/iter33-验收审计-2026-10-21.md` 卡① 节)。 ### 一、实际改动—验收对照表(§7.1 需求侧表的执行对账,六列+结果) @@ -16,9 +16,9 @@ | 需求点 | 改动实值(文件:行) | 验收测试实值(落点+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | 结果 | |---|---|---|---|---|---|---| | sv1 三槽承载+位间隔离 | `save_manager.gd:15-27` 常量与字段(SLOT_COUNT/SLOT_PATH_TEMPLATE/_slot_template);`:77` slot_path 唯一派生口;`:66` attach_slot_template 注入口 | 冒烟 `_smoke_save_slots` ①段 `smoke_all.gd:6858` 起〔sv1 往返×3+位间隔离〕+单元 `save_slot_isolation_test.gd`〔sv1〕(22 检查) | 接口 | `SaveManagerCore` 实例按槽 API 读写往返,不经 UI | 变异①=slot_path 派生写死 `% 1` → 三槽往返一致断言红(已执行,见变异表) | 通过 | -| sv2 启动态不读盘 | `save_manager.gd:29` `_init` 默认参改 `""`+`:48-53` `_ready` 只做旧档迁移、零读槽 | 冒烟 V2-s1/V2-s2(`smoke_all.gd:273-278`)〔sv2〕真实 boot 链+单元 `save_boot_no_activate_test.gd`(8 检查)类级白盒 | 冒烟+单元 | 组合根 boot 链 `/root/SaveManager`(真实入口=冒烟);单元为 --script 无 autoload 环境的类级对照面(**双落点对账:计划仅列单元,实测拆为冒烟真实链+单元类级**) | 变异④=_ready 偷调 activate_slot(1) → V2-s1/V2-s3 红(已执行) | 通过 | -| sv3 无激活写盘拒绝 | `save_manager.gd:354-358` `write_to_disk` 入口 `_path == ""` 判定 | 冒烟 V2-s3(`smoke_all.gd:278`)+单元〔sv3〕「拒写+返 false+盘面零触碰」(`save_boot_no_activate_test.gd` 快照三槽存在性/字节/mtime 全等) | 接口 | `MetaProgression.settle_adventure()`(`meta_progression.gd:275`,写盘 :282)无激活态触达写盘被拒 | 撤销=去掉判定放行旧入口 → 拒写断言红(代码评审可证:判定即 sv3 唯一闸门) | 通过 | -| sv4 新档原语 | `save_manager.gd:116-137` `create_slot`(创建即激活+落盘+失败回退) | 冒烟 ③段〔sv4〕全零可玩+文件在位 | 接口 | 空槽调新档 API 后按域读回 | 变异=新档写成读旧档 → 全零断言红(白板态断言锚 `_default_data` 全零,读旧档即带异值必红——与 sv5 读回断言同族) | 通过 | +| sv2 启动态不读盘 | `save_manager.gd:29` `_init` 默认参改 `""`+`:48-53` `_ready` 只做旧档迁移、零读槽 | 冒烟 V2-s1/V2-s2(`smoke_all.gd:273-278`)〔sv2〕真实 boot 链+单元 `save_boot_no_activate_test.gd`(12 检查)类级白盒+生产链对照 | 冒烟+单元 | 组合根 boot 链 `/root/SaveManager`(真实入口=冒烟);单元为 --script 无 autoload 环境的类级对照面+settle 生产链(**双落点对账:计划仅列单元,实测拆为冒烟真实链+单元类级/链级**) | 变异④=_ready 偷调 activate_slot(1) → V2-s1/V2-s3 红(已执行) | 通过 | +| sv3 无激活写盘拒绝 | `save_manager.gd:354-358` `write_to_disk` 入口 `_path == ""` 判定 | 冒烟 V2-s3(`smoke_all.gd:278`)+单元〔sv3〕「拒写+返 false+三槽快照全等+**退化写目标(裸 .tmp)零新增**+**MetaProgression.settle_adventure 生产链触达后盘面零触碰**」(`save_boot_no_activate_test.gd`,12 检查) | 接口 | `MetaProgression.settle_adventure()`(`meta_progression.gd:275`,写盘 :282)无激活态触达写盘被拒——生产链对照面在单测 ③段实测(真表 db 驱动全链) | 变异⑦=闸短路(`if false and _path == ""`)→ 退化写目标两断言红(类级+链级,已执行;首轮断言无判别力经审计判缺漏后强化) | 通过(回炉后) | +| sv4 新档原语 | `save_manager.gd:116-137` `create_slot`(创建即激活+落盘+失败回退) | 冒烟 ③段〔sv4〕非白板旧内容(999/5)覆盖为全零白板+盘面核验非旧值 | 接口 | 空槽调新档 API 后按域读回+盘面文件内容核验 | 变异⑥=create 改读旧档(`load_from_disk()` 代 `_default_data()`)→ 白板两断言红(已执行) | 通过(回炉后) | | sv5 进档原语 | `save_manager.gd:94-114` `activate_slot`(激活+读盘+失败恢复切换前三态) | 冒烟 ①④段〔sv5〕+单元〔sv5〕 | 接口 | 槽 N 有档 → activate_slot → 读回=该槽内容 | 变异⑤=activate 漏更新 _path → 写盘受理断言红(已执行) | 通过 | | sv6 删档原语 | `save_manager.gd:140-161` `delete_slot`(文件删除+.tmp 清偿+激活指针连清除+.corrupt.bak 不追删) | 冒烟 ②段〔sv6〕文件系统存在性断言 | 集成 | 激活/非激活槽调删档 API+盘面核查 | 变异②=漏删文件 → 在盘断言红(已执行) | 通过 | | sv7 摘要三项按槽窥视 | `save_manager.gd:163-168` `peek_slot_summary`+`:170-172` peek_summary_at 注入口+`:174-205` 只读解析 | 单元 `save_slot_summary_test.gd`〔sv7〕(12 检查:三槽异值/空槽/三不可读态)+冒烟 ⑥段 | 单元+接口 | 窥视 API 按槽调用(不激活不写盘——套件内断言 active_slot==0) | 变异③=窥视改读激活态内存 → 跨槽摘要红(已执行) | 通过 | @@ -39,6 +39,8 @@ | ③ | `peek_slot_summary` 改读激活态内存 | 跨槽摘要红 | sv7 窥视三项逐槽正确·槽 1/2 红(`save_slot_summary_test`) | 单元套件直跑 | all green (12 checks) | | ④ | `_ready` 偷调 `activate_slot(1)`(偷复激活) | boot 断言红 | V2-s1 boot 后无激活态红+V2-s3 写盘拒绝红(冒烟) | `run_smoke.py` 全量 | 1201/0 全绿 | | ⑤ | `activate_slot` 漏更新 `_path`(写盘落旧目标) | 写盘落错槽红 | sv3 激活态写盘受理·槽 1/2 红(`save_slot_isolation_test`——无激活态拒写路径被触发) | 单元套件直跑 | all green (22 checks) | +| ⑥ | `create_slot` 改读旧档(`load_from_disk()` 代 `_default_data()`,首轮审计 sv4 缺漏回炉) | 全零白板断言红 | sv4 白板态红+sv4 盘面核验红(999 旧值带入;冒烟) | `run_smoke.py` 全量 | 1201/0 全绿 | +| ⑦ | `write_to_disk` 无激活闸短路(首轮审计 sv3 缺漏回炉——拆闸实验证实原断言无判别力) | 拒写盘面零触碰断言红 | sv3 退化写目标零触碰红+sv3 生产链盘面零触碰红(`save_boot_no_activate_test`——杂散 game/.tmp 真实落盘被两锚抓获) | 单元套件直跑 | all green (12 checks) | ### 三、T0 产物:隔离面全清单清点(sv11 件核) @@ -71,6 +73,6 @@ | 闸 | 基线(T0) | 末态(卡① 收口) | |---|---|---| | 冒烟 | 1156 / 0 | **1201 / 0**(+45:V2-s1..s4 四断言+_smoke_save_slots 段 41 断言) | -| 单元 | 35 套件全绿 | **38 套件全绿**(+3:isolation 22 / boot 8 / summary 12) | +| 单元 | 35 套件全绿 | **38 套件全绿**(+3:isolation 22 / boot 12〔回炉+4〕/ summary 12) | | probe --click | 全绿 | 全绿(重闸复跑) | | 重闸 --code | — | 全绿(回执随合并提交留痕) |