|
|
@@ -0,0 +1,439 @@
|
|
|
+# Exam Sprint Report Filename Implementation Plan
|
|
|
+
|
|
|
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
+
|
|
|
+**Goal:** Generate 临考词汇突击 PDF filenames as `{StudentNameOrReportId}-临考词汇突击成果报告/潜力展望报告-{AsiaShanghaiTimestamp}.pdf`.
|
|
|
+
|
|
|
+**Architecture:** Keep filename generation centralized in `ExamSprintReportGenerationPipeline`, because it already builds the upload filename and has access to `ExamSprintReport`, `ReportType`, content, and the injected `Clock`. Propagate optional lowercase `studentName` through the achievement contract/domain mapper, add lowercase alias validation support for outlook, and stop default in-memory storage from parsing report id out of localized filenames. Tests should lock worker-level generation, service-level sync behavior, and in-memory storage URL generation so persisted `storageObjectKey`, persisted `fileName`, and download behavior remain consistent.
|
|
|
+
|
|
|
+**Tech Stack:** Java 17, Maven, JUnit 5, AssertJ, Jackson `JsonNode`, Spring Boot test utilities.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Worker-level filename tests
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java`
|
|
|
+
|
|
|
+**Step 1: Write failing tests and update existing expectations**
|
|
|
+
|
|
|
+Update `processMarksReportSuccessAfterUpload()` to use an outlook payload containing root `studentName`:
|
|
|
+
|
|
|
+```java
|
|
|
+repository.save(ExamSprintReport.pending(
|
|
|
+ "report-success",
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ unmodeledOutlookContentWithStudentName("冯亿豪"),
|
|
|
+ FIXED_CLOCK.instant(),
|
|
|
+ FIXED_CLOCK.instant().plusSeconds(86400)));
|
|
|
+```
|
|
|
+
|
|
|
+Expect:
|
|
|
+
|
|
|
+```java
|
|
|
+assertThat(report.storageObjectKey()).isEqualTo("冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf");
|
|
|
+assertThat(report.fileName()).isEqualTo("冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf");
|
|
|
+```
|
|
|
+
|
|
|
+Update `processLogsSuccessfulGenerationStages(...)` to expect:
|
|
|
+
|
|
|
+```java
|
|
|
+.contains("storageObjectKey=report-log-success-临考词汇突击潜力展望报告-20260101080000.pdf")
|
|
|
+```
|
|
|
+
|
|
|
+because its helper can keep an empty payload to exercise fallback behavior.
|
|
|
+
|
|
|
+Update `processCreatesAchievementFileNameAndStorageKeyAfterUpload()` to use achievement content with `studentName = "吴泓妤"`. Expect:
|
|
|
+
|
|
|
+```java
|
|
|
+assertThat(report.fileName()).isEqualTo("吴泓妤-临考词汇突击成果报告-20260101080000.pdf");
|
|
|
+assertThat(report.storageObjectKey()).isEqualTo("吴泓妤-临考词汇突击成果报告-20260101080000.pdf");
|
|
|
+```
|
|
|
+
|
|
|
+Add a focused fallback test:
|
|
|
+
|
|
|
+```java
|
|
|
+@Test
|
|
|
+void processFallsBackToReportIdWhenStudentNameIsBlank() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ repository.save(ExamSprintReport.pending(
|
|
|
+ "report-blank-student",
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ unmodeledOutlookContentWithStudentName(" "),
|
|
|
+ FIXED_CLOCK.instant(),
|
|
|
+ FIXED_CLOCK.instant().plusSeconds(86400)));
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ ExamSprintReportGenerationWorker worker = createWorker(repository, List.of(new TestRenderer()), storage);
|
|
|
+
|
|
|
+ worker.process("report-blank-student");
|
|
|
+
|
|
|
+ ExamSprintReport report = repository.findById("report-blank-student").orElseThrow();
|
|
|
+ assertThat(report.fileName()).isEqualTo("report-blank-student-临考词汇突击潜力展望报告-20260101080000.pdf");
|
|
|
+ assertThat(report.storageObjectKey()).isEqualTo(report.fileName());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Add helper:
|
|
|
+
|
|
|
+```java
|
|
|
+private ReportContent unmodeledOutlookContentWithStudentName(String studentName) {
|
|
|
+ return new UnmodeledReportContent(
|
|
|
+ ReportType.OUTLOOK,
|
|
|
+ OBJECT_MAPPER.createObjectNode().put("studentName", studentName));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: Run worker tests to verify failure**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportGenerationWorkerTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL with assertions showing old filenames like `exam-sprint-outlook-report-report-success.pdf`.
|
|
|
+
|
|
|
+### Task 2: Propagate achievement `studentName`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/AchievementExamSprintReportPayload.java`
|
|
|
+- Modify: `abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/AchievementReportContent.java`
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java`
|
|
|
+- Modify tests that instantiate `AchievementExamSprintReportPayload` or `AchievementReportContent` directly.
|
|
|
+
|
|
|
+**Step 1: Add optional contract field**
|
|
|
+
|
|
|
+Add lowercase `studentName` as the first record component without validation annotations:
|
|
|
+
|
|
|
+```java
|
|
|
+public record AchievementExamSprintReportPayload(
|
|
|
+ String studentName,
|
|
|
+ @NotBlank String reportTitle,
|
|
|
+ ...)
|
|
|
+```
|
|
|
+
|
|
|
+Because Java records use positional constructors, update existing `new AchievementExamSprintReportPayload(...)` calls by adding a first argument such as `"吴泓妤"` in happy-path tests or `null` where the name is irrelevant.
|
|
|
+
|
|
|
+**Step 2: Add optional domain field**
|
|
|
+
|
|
|
+Add `String studentName` as the first `AchievementReportContent` component:
|
|
|
+
|
|
|
+```java
|
|
|
+public record AchievementReportContent(
|
|
|
+ String studentName,
|
|
|
+ String reportTitle,
|
|
|
+ ...)
|
|
|
+```
|
|
|
+
|
|
|
+Do not add `Objects.requireNonNull(studentName, ...)`; missing names must be valid.
|
|
|
+
|
|
|
+**Step 3: Map the field**
|
|
|
+
|
|
|
+In `AchievementReportContentMapper.toDomainContent(...)`, pass `payload.studentName()` into the new first domain constructor argument.
|
|
|
+
|
|
|
+**Step 4: Run focused compile/test**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=AchievementReportContentMapperTest,ExamSprintReportGenerationWorkerTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: compilation succeeds after all constructor call sites are updated. Worker filename assertions may still fail until Task 3 implements the pipeline filename rule.
|
|
|
+
|
|
|
+### Task 3: Implement pipeline filename generation
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java`
|
|
|
+
|
|
|
+**Step 1: Add imports**
|
|
|
+
|
|
|
+Add:
|
|
|
+
|
|
|
+```java
|
|
|
+import cn.yunzhixue.ability.center.examsprint.domain.report.AchievementReportContent;
|
|
|
+import cn.yunzhixue.ability.center.examsprint.domain.report.ReportContent;
|
|
|
+import cn.yunzhixue.ability.center.examsprint.domain.report.UnmodeledReportContent;
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+```
|
|
|
+
|
|
|
+`ReportContent` and `UnmodeledReportContent` may already be absent from this file; only add what is needed.
|
|
|
+
|
|
|
+**Step 2: Add constants**
|
|
|
+
|
|
|
+Near the logger:
|
|
|
+
|
|
|
+```java
|
|
|
+private static final ZoneId REPORT_FILENAME_ZONE = ZoneId.of("Asia/Shanghai");
|
|
|
+private static final DateTimeFormatter REPORT_FILENAME_TIMESTAMP_FORMATTER =
|
|
|
+ DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(REPORT_FILENAME_ZONE);
|
|
|
+```
|
|
|
+
|
|
|
+**Step 3: Replace `fileNameOf(...)`**
|
|
|
+
|
|
|
+Replace the existing method with:
|
|
|
+
|
|
|
+```java
|
|
|
+private String fileNameOf(ExamSprintReport report) {
|
|
|
+ return studentNameOrReportId(report) + "-" + reportTitle(report.reportType()) + "-"
|
|
|
+ + REPORT_FILENAME_TIMESTAMP_FORMATTER.format(clock.instant()) + ".pdf";
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Step 4: Add helper methods**
|
|
|
+
|
|
|
+Add:
|
|
|
+
|
|
|
+```java
|
|
|
+private String studentNameOrReportId(ExamSprintReport report) {
|
|
|
+ return studentNameFromContent(report.content())
|
|
|
+ .filter(name -> !name.isBlank())
|
|
|
+ .map(String::trim)
|
|
|
+ .orElse(report.reportId());
|
|
|
+}
|
|
|
+
|
|
|
+private Optional<String> studentNameFromContent(ReportContent content) {
|
|
|
+ if (content instanceof UnmodeledReportContent unmodeledReportContent
|
|
|
+ && unmodeledReportContent.source() instanceof JsonNode payload) {
|
|
|
+ JsonNode studentName = payload.get("studentName");
|
|
|
+ if (studentName != null && studentName.isTextual()) {
|
|
|
+ return Optional.of(studentName.asText());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (content instanceof AchievementReportContent achievementReportContent) {
|
|
|
+ return Optional.ofNullable(achievementReportContent.studentName());
|
|
|
+ }
|
|
|
+ return Optional.empty();
|
|
|
+}
|
|
|
+
|
|
|
+private String reportTitle(ReportType reportType) {
|
|
|
+ return switch (reportType) {
|
|
|
+ case ACHIEVEMENT -> "临考词汇突击成果报告";
|
|
|
+ case OUTLOOK -> "临考词汇突击潜力展望报告";
|
|
|
+ };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Step 5: Run worker tests**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportGenerationWorkerTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+### Task 4: Service-level sync tests
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java`
|
|
|
+
|
|
|
+**Step 1: Update sync filename expectations**
|
|
|
+
|
|
|
+Update `createOutlookReportSyncGeneratesUploadAndReturnsDownloadUrl()` to add lowercase `studentName` to the existing valid outlook payload and expect the Shanghai timestamp. Because `FIXED_CLOCK = 2026-01-02T00:00:00Z`, expected timestamp is `20260102080000`:
|
|
|
+
|
|
|
+```java
|
|
|
+ObjectNode payload = validOutlookPayload();
|
|
|
+payload.put("studentName", "冯亿豪");
|
|
|
+
|
|
|
+var response = service.createOutlookReportSync(payload);
|
|
|
+
|
|
|
+String expectedFileName = "冯亿豪-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
+assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
|
+assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
|
+assertThat(storage.generatedKeys).containsExactly(expectedFileName);
|
|
|
+```
|
|
|
+
|
|
|
+Update `createOutlookReportSyncAcceptsCallerVocabularyPayloadAndReturnsDownloadUrl()` similarly if it should assert the filename, otherwise keep its existing renderer-focused assertions and add a separate filename test.
|
|
|
+
|
|
|
+Update `createAchievementReportSyncGeneratesUploadAndReturnsDownloadUrl()` using the `studentName` from `validAchievementPayload()`:
|
|
|
+
|
|
|
+```java
|
|
|
+String expectedFileName = "吴泓妤-临考词汇突击成果报告-20260102080000.pdf";
|
|
|
+```
|
|
|
+
|
|
|
+**Step 2: Add missing-name fallback service test**
|
|
|
+
|
|
|
+Add a sync test for outlook payload missing `studentName`:
|
|
|
+
|
|
|
+```java
|
|
|
+@Test
|
|
|
+void createOutlookReportSyncFallsBackToReportIdWhenStudentNameIsMissing() {
|
|
|
+ TestRepository repository = new TestRepository();
|
|
|
+ TestStorage storage = new TestStorage();
|
|
|
+ DefaultExamSprintReportApplicationService service = service(
|
|
|
+ repository,
|
|
|
+ reportId -> {
|
|
|
+ throw new IllegalStateException("sync create must not dispatch async generation");
|
|
|
+ },
|
|
|
+ storage);
|
|
|
+ ObjectNode payload = validOutlookPayload().deepCopy();
|
|
|
+ payload.remove("studentName");
|
|
|
+
|
|
|
+ var response = service.createOutlookReportSync(payload);
|
|
|
+
|
|
|
+ String expectedFileName = response.reportId() + "-临考词汇突击潜力展望报告-20260102080000.pdf";
|
|
|
+ ExamSprintReport saved = repository.findById(response.reportId()).orElseThrow();
|
|
|
+ assertThat(saved.storageObjectKey()).isEqualTo(expectedFileName);
|
|
|
+ assertThat(saved.fileName()).isEqualTo(expectedFileName);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+If `validOutlookPayload()` uses `StudentName` instead of lowercase `studentName`, add lowercase `studentName` to the test payload explicitly for the new naming contract, and keep renderer-specific `StudentName` assertions unchanged.
|
|
|
+
|
|
|
+**Step 3: Run application service tests**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportApplicationServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+### Task 5: Outlook validation alias and trim review fixes
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/OutlookExamSprintReportPayload.java`
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java`
|
|
|
+- Modify: `abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java`
|
|
|
+- Modify: `abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java`
|
|
|
+
|
|
|
+**Step 1: Add outlook lowercase alias without global ObjectMapper relaxation**
|
|
|
+
|
|
|
+Add `@JsonAlias("studentName")` to the existing `@JsonProperty("StudentName")` component in `OutlookExamSprintReportPayload`. Do not disable `FAIL_ON_UNKNOWN_PROPERTIES` globally in tests or production.
|
|
|
+
|
|
|
+**Step 2: Keep validation side effects local**
|
|
|
+
|
|
|
+When both `StudentName` and lowercase `studentName` exist, validate a copied payload that removes lowercase `studentName`, while preserving the original raw payload for persistence and filename generation.
|
|
|
+
|
|
|
+**Step 3: Trim filename student names**
|
|
|
+
|
|
|
+In `ExamSprintReportGenerationPipeline`, trim `studentName` before deciding whether to use it or fall back to `reportId`.
|
|
|
+
|
|
|
+**Step 4: Add trim coverage**
|
|
|
+
|
|
|
+Add a worker-level test where `studentName = " 冯亿豪 "` yields `冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf`.
|
|
|
+
|
|
|
+**Step 5: Verify**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application -Dtest=ExamSprintReportGenerationWorkerTest,ExamSprintReportApplicationServiceTest test
|
|
|
+mvn -pl abilities/exam-sprint/application test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+### Task 6: In-memory storage localized filename compatibility
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java`
|
|
|
+- Create: `abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorageTest.java`
|
|
|
+
|
|
|
+**Step 1: Write failing storage test**
|
|
|
+
|
|
|
+Create a test that uploads `fileName = "冯亿豪-临考词汇突击潜力展望报告-20260102080000.pdf"` with `reportId = "report-001"`, then expects `generateDownloadUrl(...)` to return `/api/exam-sprint/reports/report-001/download`.
|
|
|
+
|
|
|
+Also cover `download(...)` returning the original localized filename, `application/pdf`, and defensive byte copies.
|
|
|
+
|
|
|
+**Step 2: Verify RED**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/infrastructure -Dtest=InMemoryExamSprintReportStorageTest test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL because old in-memory storage parses report id from the legacy filename prefix.
|
|
|
+
|
|
|
+**Step 3: Implement storage fix**
|
|
|
+
|
|
|
+Store the `reportId` supplied to `upload(...)` in the in-memory stored record and have `generateDownloadUrl(...)` use that stored value instead of parsing `storageObjectKey`.
|
|
|
+
|
|
|
+**Step 4: Verify GREEN**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/infrastructure -Dtest=InMemoryExamSprintReportStorageTest test
|
|
|
+mvn -pl abilities/exam-sprint/infrastructure test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+### Task 7: Regression verification
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- No code changes expected.
|
|
|
+
|
|
|
+**Step 1: Run application module tests**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -pl abilities/exam-sprint/application test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS.
|
|
|
+
|
|
|
+**Step 2: Run full test suite**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn test
|
|
|
+```
|
|
|
+
|
|
|
+Expected: BUILD SUCCESS.
|
|
|
+
|
|
|
+**Step 3: Review diff**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+git diff -- abilities/exam-sprint/application docs/superpowers/specs/2026-04-29-exam-sprint-report-filename-design.md docs/plans/2026-04-29-exam-sprint-report-filename.md
|
|
|
+```
|
|
|
+
|
|
|
+Expected: Diff only contains filename tests, pipeline filename generation, and the docs for this change.
|
|
|
+
|
|
|
+### Task 8: Commit if requested
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Commit all modified files from this plan only.
|
|
|
+
|
|
|
+**Step 1: Check status**
|
|
|
+
|
|
|
+Run:
|
|
|
+
|
|
|
+```bash
|
|
|
+git status --short
|
|
|
+```
|
|
|
+
|
|
|
+Expected: Only the planned docs, test, and pipeline files are modified or untracked.
|
|
|
+
|
|
|
+**Step 2: Commit only if the user explicitly asks**
|
|
|
+
|
|
|
+Run only after explicit commit approval:
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-04-29-exam-sprint-report-filename-design.md \
|
|
|
+ docs/plans/2026-04-29-exam-sprint-report-filename.md \
|
|
|
+ abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationPipeline.java \
|
|
|
+ abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/AchievementReportContentMapper.java \
|
|
|
+ abilities/exam-sprint/application/src/main/java/cn/yunzhixue/ability/center/examsprint/application/report/DefaultExamSprintReportApplicationService.java \
|
|
|
+ abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/AchievementExamSprintReportPayload.java \
|
|
|
+ abilities/exam-sprint/contracts/src/main/java/cn/yunzhixue/ability/center/examsprint/contracts/report/OutlookExamSprintReportPayload.java \
|
|
|
+ abilities/exam-sprint/domain/src/main/java/cn/yunzhixue/ability/center/examsprint/domain/report/AchievementReportContent.java \
|
|
|
+ abilities/exam-sprint/infrastructure/src/main/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorage.java \
|
|
|
+ abilities/exam-sprint/infrastructure/src/test/java/cn/yunzhixue/ability/center/examsprint/infrastructure/report/storage/InMemoryExamSprintReportStorageTest.java \
|
|
|
+ abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportGenerationWorkerTest.java \
|
|
|
+ abilities/exam-sprint/application/src/test/java/cn/yunzhixue/ability/center/examsprint/application/report/ExamSprintReportApplicationServiceTest.java
|
|
|
+git commit -m "fix(exam-sprint): 调整报告 PDF 文件名"
|
|
|
+```
|
|
|
+
|
|
|
+Expected: Commit succeeds without bypassing hooks.
|