2026-04-30-defensive-programming-pass1.md 5.1 KB

Defensive Programming Pass 1 Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 以保守方式收敛两处明显的过度防御:把 AchievementReportContenthitWords 归一化为非 null,不再让 renderer 对内部不变量重复判空;让 AchievementReportContentMapper 对不应出现的 stageName == null fail-fast,而不是静默兜底成空字符串。

Architecture: 本轮只收敛“内部不变量已建立后仍重复兜底”的分支,不改 HTTP 契约、不改输入校验总策略。实现上通过领域对象归一化 hitWords 消除下游 null 防御,再通过 mapper 的 fail-fast 去掉 stageName 空串兜底。

Tech Stack: Java 21, Maven, JUnit 5, AssertJ, Spring Boot


Task 1: Normalize achievement hit words to an immutable non-null list

Files:

  • Modify: abilities/exam-sprint/domain/src/test/java/cn/yunzhixue/ability/center/examsprint/domain/report/AchievementReportContentTest.java
  • Modify: abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/AchievementReportContent.java
  • Modify: abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/rendering/achievement/ClasspathAchievementExamSprintReportRenderer.java

Step 1: Write the failing test

AchievementReportContentTest 增加一个测试,断言当 ExamUnknownWordsHitStatus 传入 nullhitWords 时:

  • hitWords() 返回空列表而不是 null
  • 返回值不可变

测试名建议:normalizesNullHitWordsToEmptyImmutableList

Step 2: Run test to verify it fails

Run: mvn -q -pl abilities/exam-sprint/domain -Dtest=AchievementReportContentTest#normalizesNullHitWordsToEmptyImmutableList test

Expected: FAIL,因为当前实现会保留 null

Step 3: Write minimal implementation

AchievementReportContent.ExamUnknownWordsHitStatus 构造器中,把 hitWords 归一化为 List.of()List.copyOf(...) 的结果,保证字段永远非 null 且不可变。

然后在 ClasspathAchievementExamSprintReportRenderer.renderHitWords(...) 中删掉对 hitStatus == nullhitStatus.hitWords() == null 的重复防御,只保留对空列表的空态渲染。

Step 4: Run test to verify it passes

Run: mvn -q -pl abilities/exam-sprint/domain -Dtest=AchievementReportContentTest#normalizesNullHitWordsToEmptyImmutableList test

Expected: PASS

Step 5: Run focused regression tests

Run: mvn -q -pl abilities/exam-sprint/infrastructure -am -Dtest=ClasspathAchievementExamSprintReportRendererTest test

Expected: PASS,尤其覆盖“hitWords 为空 / null 时渲染空态”的现有行为

Task 2: Fail fast on impossible null stage name in achievement mapper

Files:

  • Modify: abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapperTest.java
  • Modify: abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java

Step 1: Write the failing test

AchievementReportContentMapperTest 增加一个测试,构造 AchievementExamSprintReportPayload,其中 stageNamenull,并断言 toDomainContent(...) 抛出 NullPointerException 且消息包含 stageName

测试名建议:rejectsNullStageNameInsteadOfSilentlyFallingBack

Step 2: Run test to verify it fails

Run: mvn -q -pl abilities/exam-sprint/application -am -Dtest=AchievementReportContentMapperTest#rejectsNullStageNameInsteadOfSilentlyFallingBack test

Expected: FAIL,因为当前实现会把 null 兜底为 ""

Step 3: Write minimal implementation

AchievementReportContentMapper.reportTitle(...) 中用 Objects.requireNonNull(stageName, "stageName") 明确内部不变量,再对非 null 字符串做 trim()

不要顺手调整其他 mapper/null 策略。

Step 4: Run test to verify it passes

Run: mvn -q -pl abilities/exam-sprint/application -am -Dtest=AchievementReportContentMapperTest#rejectsNullStageNameInsteadOfSilentlyFallingBack test

Expected: PASS

Step 5: Run focused regression tests

Run: mvn -q -pl abilities/exam-sprint/application -am -Dtest=AchievementReportContentMapperTest test

Expected: PASS

Task 3: Final verification for the conservative pass

Files:

  • Verify only; no additional code changes expected

Step 1: Run the targeted module tests together

Run: mvn -q -pl abilities/exam-sprint/domain,abilities/exam-sprint/application,abilities/exam-sprint/infrastructure -am -Dtest=AchievementReportContentTest,AchievementReportContentMapperTest,ClasspathAchievementExamSprintReportRendererTest test

Expected: PASS

Step 2: Run full repository tests

Run: mvn test -q

Expected: PASS

Step 3: Review diff for scope control

Run: git diff -- abilities/exam-sprint/domain abilities/exam-sprint/application abilities/exam-sprint/infrastructure docs/plans/2026-04-30-defensive-programming-pass1.md

Expected: 只包含计划内的保守收敛改动