feat: preserve due operation provenance (AEG-V15-035)

Carries scheduledFor, catch-up policy, and maxCatchUp from the scheduler through the request model and transactional outbox. Evidence: targeted Release tests 5/5 passed; TRX SHA256 C1BF3EF274702305A29673D5B6A1C3A98D08B1716DA3CD8CB0EE710B5E6C12E6. Schedules remain disabled.
This commit is contained in:
2026-08-09 02:04:26 +09:00
parent dd352596fc
commit d18f6a7a67
10 changed files with 168 additions and 5 deletions
@@ -0,0 +1,21 @@
# AEG-V15-035 — Due operation contract
## Scope
- **WBS ID:** `AEG-V15-035`; **Slice:** VS-18 / Scheduler
- **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-03` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-03`
- **Dependency / Gate:** `AEG-V15-034` (completed, commit `dd35259`) / `G3`
- **Acceptance:** persist and trace `scheduledFor`, catch-up policy, and `maxCatchUp` for each model-operation request.
## Source / Assumption / Unknown / Decision Required
- **Source:** `db/migrations/0020_v15_execution_and_ui_contract_hardening.sql` adds nullable `evaluation.model_operation_request.scheduled_for`; `DueModelOperation` already receives `ScheduledFor`, `CatchUpPolicy`, and `MaxCatchUp`; the dispatcher and request repository currently drop this provenance before insert.
- **Assumption:** an individual request must preserve the selected occurrence's UTC scheduled time and the policy/configuration that selected it. These are immutable request provenance, not mutable schedule state.
- **Unknown:** `scheduled_for` is nullable in the approved migration. This Slice will populate it for scheduler-created requests but will not change historical rows or alter the immutable migration.
- **Decision Required:** a full per-occurrence dispatch ledger and enqueue/mark transactional outbox remain `DEC-083` / later scheduler work; this Slice only closes the currently missing request-level provenance.
## Actual evidence
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter "FullyQualifiedName~ScheduleOccurrencePlannerTests|FullyQualifiedName~ModelOperationRequestServiceTests" --logger "trx;LogFileName=DueModelOperationContractTests_20260809.trx" --results-directory evidence/AEG-V15-035`
- Result: passed `5/5`; artifact: `evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx`; SHA-256: `C1BF3EF274702305A29673D5B6A1C3A98D08B1716DA3CD8CB0EE710B5E6C12E6`.
- The targeted build compiles Hangfire serialization, application validation, the append-only request insert, transactional outbox payload, and the scheduler policy tests. No schedule was enabled and no database migration or PostgreSQL test is claimed.
+1 -1
View File
@@ -513,7 +513,7 @@ AEG-V15-031,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-07,Cross,Cro
AEG-V15-032,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-08,Cross,Cross,Cross,UI-V15-LAY-08,T-V15-LAY-08,디자인 token 변경 Gate,semantic token contract,기능 CSS 직접 색상/간격 drift 0,UX Lead,FE/QA,4,AEG-V15-031,MVP-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
AEG-V15-033,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-01,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-01,Schedule anchor 계산기,ScheduleOccurrencePlanner,dispatch 지연이 cadence drift를 만들지 않음,BE Lead,SRE/QA,3,AEG-X-001,G3,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
AEG-V15-034,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-02,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-02,Catch-up policy 구현,LATEST/SKIP/ALL_LIMIT,장애 복구 후 job storm 0,BE Lead,SRE/QA,5,AEG-V15-033,G3,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
AEG-V15-035,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-03,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-03,Due operation 계약 확장,DueModelOperation v15,scheduledFor/catchUp/maxCatchUp 추적,BE Lead,SRE/QA,5,AEG-V15-034,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
AEG-V15-035,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-03,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-03,Due operation 계약 확장,DueModelOperation v15,scheduledFor/catchUp/maxCatchUp 추적,BE Lead,SRE/QA,5,AEG-V15-034,G3,SOURCE+DESIGN_PROPOSAL,P1,COMPLETED
AEG-V15-036,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-04,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-04,Dispatcher nextDue CAS,schedule repository,lease 상실·중복 advance 0,BE Lead,SRE/QA,6,AEG-V15-035,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
AEG-V15-037,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-05,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-05,BusinessHold와 기술실패 분리,execution state machine,"blind retry 0, reason/holdUntil 기록",BE Lead,SRE/QA,5,AEG-V15-036,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
AEG-V15-038,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-06,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-06,Schedule heartbeat/aging,execution heartbeat,stuck request 탐지와 Owner alert,BE Lead,SRE/QA,6,AEG-V15-037,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
1 WBS_ID Sprint Weeks Slice_ID Slice Module Workstream Requirement_ID API_ID DB_Migration_ID Job_Event_ID UI_ID Test_ID Task Artifact Acceptance_Evidence Primary_Owner Secondary PD Dependency Gate Evidence_Class Risk Status
513 AEG-V15-032 S6 W13-14 Cross Cross-cutting UX/Layout LAY REQ-V15-LAY-08 Cross Cross Cross UI-V15-LAY-08 T-V15-LAY-08 디자인 token 변경 Gate semantic token contract 기능 CSS 직접 색상/간격 drift 0 UX Lead FE/QA 4 AEG-V15-031 MVP-A SOURCE+DESIGN_PROPOSAL P1 PLANNED
514 AEG-V15-033 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-01 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-01 Schedule anchor 계산기 ScheduleOccurrencePlanner dispatch 지연이 cadence drift를 만들지 않음 BE Lead SRE/QA 3 AEG-X-001 G3 SOURCE+DESIGN_PROPOSAL P0 COMPLETED
515 AEG-V15-034 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-02 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-02 Catch-up policy 구현 LATEST/SKIP/ALL_LIMIT 장애 복구 후 job storm 0 BE Lead SRE/QA 5 AEG-V15-033 G3 SOURCE+DESIGN_PROPOSAL P0 COMPLETED
516 AEG-V15-035 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-03 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-03 Due operation 계약 확장 DueModelOperation v15 scheduledFor/catchUp/maxCatchUp 추적 BE Lead SRE/QA 5 AEG-V15-034 G3 SOURCE+DESIGN_PROPOSAL P1 PLANNED COMPLETED
517 AEG-V15-036 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-04 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-04 Dispatcher nextDue CAS schedule repository lease 상실·중복 advance 0 BE Lead SRE/QA 6 AEG-V15-035 G3 SOURCE+DESIGN_PROPOSAL P0 PLANNED
518 AEG-V15-037 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-05 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-05 BusinessHold와 기술실패 분리 execution state machine blind retry 0, reason/holdUntil 기록 BE Lead SRE/QA 5 AEG-V15-036 G3 SOURCE+DESIGN_PROPOSAL P1 PLANNED
519 AEG-V15-038 S8 W17-18 VS-18 BacktestingEvaluation Scheduler SCH REQ-V15-SCH-06 V15-SCH-API MIG-0020 J31~J40 Cross T-V15-SCH-06 Schedule heartbeat/aging execution heartbeat stuck request 탐지와 Owner alert BE Lead SRE/QA 6 AEG-V15-037 G3 SOURCE+DESIGN_PROPOSAL P0 PLANNED
@@ -8,6 +8,7 @@ AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
AEG-V15-033,S8,VS-18,Schedule anchor 계산기,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-033_SCHEDULE_ANCHOR_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-033/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ScheduleOccurrencePlannerTests; 2/2 passed. Characterizes the approved scheduledFor anchor and missed-occurrence skip behavior. No schedule was enabled; market-calendar/timezone (DEC-079), dispatch enqueue/mark atomicity (DEC-083), leases, database integration, and later scheduler WBS evidence remain out of scope."
AEG-V15-034,S8,VS-18,Catch-up policy 구현,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-034_CATCH_UP_POLICY_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelScheduleRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-034/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: targeted ScheduleOccurrencePlannerTests 4/4 passed. LATEST_ONLY dispatches only the latest anchored occurrence; SKIP_MISSED advances the lease-held schedule without enqueueing stale work; ALL_WITH_LIMIT dispatches only the configured most-recent occurrences, each with an occurrence-specific UTC idempotency key. Schedules remain disabled. Database integration, lease CAS, and DEC-083 enqueue/mark atomicity are not claimed and remain owned by later WBS items."
AEG-V15-035,S8,VS-18,Due operation 계약 확장,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-035_DUE_OPERATION_CONTRACT_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Application/ModelOperationsContracts.cs; src/KArtSell.Modules.ModelOperations/Application/ModelOperationRequestService.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelOperationRequestRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationRequestServiceTests.cs; evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx",BE Lead,"Actual Release run: 5/5 targeted unit tests passed. The scheduler occurrence, catch-up policy, and max catch-up flow from due schedule through the serialized job and validated application request; scheduled_for is inserted in the normalized request model and all three values are retained in the transactional outbox payload. Schedules remain disabled. No new migration or PostgreSQL integration evidence is claimed: MIG-0020 already provides scheduled_for; policy and limit provenance is immutable in the event payload, while schedule configuration remains the normalized source referenced by schedule_id/version."
AEG-V16-017,S6,Cross,FieldShell 표준,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts","FE Lead","2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence."
AEG-V16-016,S0,VS-00,Vendor boundary fitness,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-016_VENDOR_BOUNDARY_SLICE_NOTE.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts; evidence/AEG-V16-016/validate_v16_20260808.log; evidence/AEG-V16-016/ui-adapter-tests_20260808.log; evidence/AEG-V16-016/frontend-typecheck_20260808.log","FE Lead","2026-08-08: Removed stale fixed WBS row-count assertion; validator now verifies WBS ID integrity and reports vendor imports outside the approved adapter boundary. Re-executed actual evidence: python tools/validate_v16.py PASS=1 WARN=2 FAIL=0; targeted adapter tests 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked because dependency AEG-V16-015 has no approved acceptance evidence in the tracker."
AEG-V16-015,S0,VS-00,Adapter rollback runbook,BLOCKED,-,"docs/CURRENT/ui-provider-switch.md","FE Lead","2026-08-08: Runbook exists, but status is BLOCKED before completion: acceptance requires visual/a11y/performance rollback rehearsal evidence, which is not present; direct dependency AEG-V16-014 has no tracker evidence. A runbook does not substitute for an approved visual baseline, keyboard/focus and accessible-name report, state-matrix result, agreed performance budget, immutable-artifact rollback rehearsal, and append-only release evidence. No build/test/migration claimed by this status correction."
1 WBS_ID Sprint Slice_ID Task Status Completion_Date Evidence_Link Owner Notes
8 AEG-X-007 S0 Cross Serilog/OTel correlation 고도화 COMPLETED 2026-08-06 tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db SRE/Security ✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253).
9 AEG-V15-033 S8 VS-18 Schedule anchor 계산기 COMPLETED 2026-08-09 docs/CURRENT/AEG-V15-033_SCHEDULE_ANCHOR_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-033/ScheduleOccurrencePlannerTests_20260809.trx BE Lead Actual Release run: dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ScheduleOccurrencePlannerTests; 2/2 passed. Characterizes the approved scheduledFor anchor and missed-occurrence skip behavior. No schedule was enabled; market-calendar/timezone (DEC-079), dispatch enqueue/mark atomicity (DEC-083), leases, database integration, and later scheduler WBS evidence remain out of scope.
10 AEG-V15-034 S8 VS-18 Catch-up policy 구현 COMPLETED 2026-08-09 docs/CURRENT/AEG-V15-034_CATCH_UP_POLICY_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelScheduleRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-034/ScheduleOccurrencePlannerTests_20260809.trx BE Lead Actual Release run: targeted ScheduleOccurrencePlannerTests 4/4 passed. LATEST_ONLY dispatches only the latest anchored occurrence; SKIP_MISSED advances the lease-held schedule without enqueueing stale work; ALL_WITH_LIMIT dispatches only the configured most-recent occurrences, each with an occurrence-specific UTC idempotency key. Schedules remain disabled. Database integration, lease CAS, and DEC-083 enqueue/mark atomicity are not claimed and remain owned by later WBS items.
11 AEG-V15-035 S8 VS-18 Due operation 계약 확장 COMPLETED 2026-08-09 docs/CURRENT/AEG-V15-035_DUE_OPERATION_CONTRACT_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Application/ModelOperationsContracts.cs; src/KArtSell.Modules.ModelOperations/Application/ModelOperationRequestService.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelOperationRequestRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationRequestServiceTests.cs; evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx BE Lead Actual Release run: 5/5 targeted unit tests passed. The scheduler occurrence, catch-up policy, and max catch-up flow from due schedule through the serialized job and validated application request; scheduled_for is inserted in the normalized request model and all three values are retained in the transactional outbox payload. Schedules remain disabled. No new migration or PostgreSQL integration evidence is claimed: MIG-0020 already provides scheduled_for; policy and limit provenance is immutable in the event payload, while schedule configuration remains the normalized source referenced by schedule_id/version.
12 AEG-V16-017 S6 Cross FieldShell 표준 IN_PROGRESS TBD docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts FE Lead 2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence.
13 AEG-V16-016 S0 VS-00 Vendor boundary fitness IN_PROGRESS TBD docs/CURRENT/AEG-V16-016_VENDOR_BOUNDARY_SLICE_NOTE.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts; evidence/AEG-V16-016/validate_v16_20260808.log; evidence/AEG-V16-016/ui-adapter-tests_20260808.log; evidence/AEG-V16-016/frontend-typecheck_20260808.log FE Lead 2026-08-08: Removed stale fixed WBS row-count assertion; validator now verifies WBS ID integrity and reports vendor imports outside the approved adapter boundary. Re-executed actual evidence: python tools/validate_v16.py PASS=1 WARN=2 FAIL=0; targeted adapter tests 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked because dependency AEG-V16-015 has no approved acceptance evidence in the tracker.
14 AEG-V16-015 S0 VS-00 Adapter rollback runbook BLOCKED - docs/CURRENT/ui-provider-switch.md FE Lead 2026-08-08: Runbook exists, but status is BLOCKED before completion: acceptance requires visual/a11y/performance rollback rehearsal evidence, which is not present; direct dependency AEG-V16-014 has no tracker evidence. A runbook does not substitute for an approved visual baseline, keyboard/focus and accessible-name report, state-matrix result, agreed performance budget, immutable-artifact rollback rehearsal, and append-only release evidence. No build/test/migration claimed by this status correction.
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="ccf00f64-17eb-4e0f-a031-3122a0c53734" name="kjh20@KIMJAEHYUN-NOTE 2026-08-09 02:03:28" runUser="KIMJAEHYUN-NOTE\kjh20" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-08-09T02:03:28.4753949+09:00" queuing="2026-08-09T02:03:28.4753953+09:00" start="2026-08-09T02:03:27.2968210+09:00" finish="2026-08-09T02:03:28.4826469+09:00" />
<TestSettings name="default" id="601a28b0-722e-43f8-a9c9-197dfd377261">
<Deployment runDeploymentRoot="kjh20_KIMJAEHYUN-NOTE_2026-08-09_02_03_28" />
</TestSettings>
<Results>
<UnitTestResult executionId="99360743-9fcc-4ace-9842-39629134ba16" testId="734f2a89-2b23-0864-3bdc-dd5d2c1e065a" testName="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.All_with_limit_dispatches_only_the_most_recent_configured_occurrences" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0136207" startTime="2026-08-09T02:03:28.3717246+09:00" endTime="2026-08-09T02:03:28.3731210+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="99360743-9fcc-4ace-9842-39629134ba16" />
<UnitTestResult executionId="181851ff-5aea-4eb7-9036-054b0c8b8f3e" testId="60773dbd-c959-2ad9-4ce3-ac2f4b7149da" testName="KArtSell.ModelOperations.UnitTests.ModelOperationRequestServiceTests.Persists_the_scheduled_occurrence_and_catch_up_provenance" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0116450" startTime="2026-08-09T02:03:28.3417137+09:00" endTime="2026-08-09T02:03:28.3636946+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="181851ff-5aea-4eb7-9036-054b0c8b8f3e" />
<UnitTestResult executionId="dda1a84f-5f59-4a9c-8b89-de3951d34b40" testId="4a0acb4c-da35-f6d9-1253-fba8b2ec4ae4" testName="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Missed_occurrences_are_skipped_without_dispatch_storm" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0003133" startTime="2026-08-09T02:03:28.3735592+09:00" endTime="2026-08-09T02:03:28.3735987+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="dda1a84f-5f59-4a9c-8b89-de3951d34b40" />
<UnitTestResult executionId="161bc9dc-3564-44cd-90f8-9f4c7a3f6ede" testId="42622f9c-8837-0fa6-cb49-0be424719bcd" testName="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Skip_missed_advances_without_dispatching_a_stale_occurrence" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0097932" startTime="2026-08-09T02:03:28.3432528+09:00" endTime="2026-08-09T02:03:28.3583069+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="161bc9dc-3564-44cd-90f8-9f4c7a3f6ede" />
<UnitTestResult executionId="be7c18a0-f97d-4836-9f7e-e633e89effcd" testId="4cc4638d-542d-f501-1295-718da6fcbfd1" testName="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Daily_anchor_does_not_drift_to_dispatch_time" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0002782" startTime="2026-08-09T02:03:28.3733082+09:00" endTime="2026-08-09T02:03:28.3733655+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="be7c18a0-f97d-4836-9f7e-e633e89effcd" />
</Results>
<TestDefinitions>
<UnitTest name="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.All_with_limit_dispatches_only_the_most_recent_configured_occurrences" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.modeloperations.unittests\bin\release\net10.0\kartsell.modeloperations.unittests.dll" id="734f2a89-2b23-0864-3bdc-dd5d2c1e065a">
<Execution id="99360743-9fcc-4ace-9842-39629134ba16" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.ModelOperations.UnitTests\bin\Release\net10.0\KArtSell.ModelOperations.UnitTests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests" name="All_with_limit_dispatches_only_the_most_recent_configured_occurrences" />
</UnitTest>
<UnitTest name="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Skip_missed_advances_without_dispatching_a_stale_occurrence" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.modeloperations.unittests\bin\release\net10.0\kartsell.modeloperations.unittests.dll" id="42622f9c-8837-0fa6-cb49-0be424719bcd">
<Execution id="161bc9dc-3564-44cd-90f8-9f4c7a3f6ede" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.ModelOperations.UnitTests\bin\Release\net10.0\KArtSell.ModelOperations.UnitTests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests" name="Skip_missed_advances_without_dispatching_a_stale_occurrence" />
</UnitTest>
<UnitTest name="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Missed_occurrences_are_skipped_without_dispatch_storm" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.modeloperations.unittests\bin\release\net10.0\kartsell.modeloperations.unittests.dll" id="4a0acb4c-da35-f6d9-1253-fba8b2ec4ae4">
<Execution id="dda1a84f-5f59-4a9c-8b89-de3951d34b40" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.ModelOperations.UnitTests\bin\Release\net10.0\KArtSell.ModelOperations.UnitTests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests" name="Missed_occurrences_are_skipped_without_dispatch_storm" />
</UnitTest>
<UnitTest name="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests.Daily_anchor_does_not_drift_to_dispatch_time" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.modeloperations.unittests\bin\release\net10.0\kartsell.modeloperations.unittests.dll" id="4cc4638d-542d-f501-1295-718da6fcbfd1">
<Execution id="be7c18a0-f97d-4836-9f7e-e633e89effcd" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.ModelOperations.UnitTests\bin\Release\net10.0\KArtSell.ModelOperations.UnitTests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.ModelOperations.UnitTests.ScheduleOccurrencePlannerTests" name="Daily_anchor_does_not_drift_to_dispatch_time" />
</UnitTest>
<UnitTest name="KArtSell.ModelOperations.UnitTests.ModelOperationRequestServiceTests.Persists_the_scheduled_occurrence_and_catch_up_provenance" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.modeloperations.unittests\bin\release\net10.0\kartsell.modeloperations.unittests.dll" id="60773dbd-c959-2ad9-4ce3-ac2f4b7149da">
<Execution id="181851ff-5aea-4eb7-9036-054b0c8b8f3e" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.ModelOperations.UnitTests\bin\Release\net10.0\KArtSell.ModelOperations.UnitTests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.ModelOperations.UnitTests.ModelOperationRequestServiceTests" name="Persists_the_scheduled_occurrence_and_catch_up_provenance" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="734f2a89-2b23-0864-3bdc-dd5d2c1e065a" executionId="99360743-9fcc-4ace-9842-39629134ba16" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="60773dbd-c959-2ad9-4ce3-ac2f4b7149da" executionId="181851ff-5aea-4eb7-9036-054b0c8b8f3e" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="4a0acb4c-da35-f6d9-1253-fba8b2ec4ae4" executionId="dda1a84f-5f59-4a9c-8b89-de3951d34b40" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="42622f9c-8837-0fa6-cb49-0be424719bcd" executionId="161bc9dc-3564-44cd-90f8-9f4c7a3f6ede" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestEntry testId="4cc4638d-542d-f501-1295-718da6fcbfd1" executionId="be7c18a0-f97d-4836-9f7e-e633e89effcd" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
</TestEntries>
<TestLists>
<TestList name="목록에 없는 결과" id="8c84fa94-04c1-424b-9868-57a2d4851a1d" />
<TestList name="로드된 모든 결과" id="19431567-8539-422a-85d7-44ee4e166bda" />
</TestLists>
<ResultSummary outcome="Completed">
<Counters total="5" executed="5" passed="5" failed="0" error="0" timeout="0" aborted="0" inconclusive="0" passedButRunAborted="0" notRunnable="0" notExecuted="0" disconnected="0" warning="0" completed="0" inProgress="0" pending="0" />
<Output>
<StdOut>[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.5+1b188a7b0a (64-bit .NET 10.0.10)&#xD;
[xUnit.net 00:00:00.50] Discovering: KArtSell.ModelOperations.UnitTests&#xD;
[xUnit.net 00:00:00.55] Discovered: KArtSell.ModelOperations.UnitTests&#xD;
[xUnit.net 00:00:00.58] Starting: KArtSell.ModelOperations.UnitTests&#xD;
[xUnit.net 00:00:00.64] Finished: KArtSell.ModelOperations.UnitTests&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
@@ -14,6 +14,9 @@ public sealed class ModelOperationRequestService(
string scopeKey,
string automationMode,
string idempotencyKey,
DateTimeOffset scheduledFor,
string catchUpPolicy,
int maxCatchUp,
string correlationId,
CancellationToken cancellationToken)
{
@@ -21,6 +24,8 @@ public sealed class ModelOperationRequestService(
ModelOperationExecutionBoundary.EnsureAllowed(definition);
if (!definition.AutomationMode.ToContractValue().Equals(automationMode, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Schedule automation mode does not match the approved operation registry.");
if (maxCatchUp is < 0 or > 31) throw new ArgumentOutOfRangeException(nameof(maxCatchUp));
if (string.IsNullOrWhiteSpace(catchUpPolicy)) throw new ArgumentException("Catch-up policy is required.", nameof(catchUpPolicy));
var now = clock.UtcNow;
var context = await contextReader.ReadAsync(scopeKey, now, cancellationToken);
@@ -35,6 +40,9 @@ public sealed class ModelOperationRequestService(
scopeKey,
definition.AutomationMode.ToContractValue(),
idempotencyKey,
scheduledFor,
catchUpPolicy,
maxCatchUp,
context,
correlationId,
now);
@@ -28,6 +28,9 @@ public sealed record ModelOperationRequest(
string ScopeKey,
string AutomationMode,
string IdempotencyKey,
DateTimeOffset ScheduledFor,
string CatchUpPolicy,
int MaxCatchUp,
ApprovedModelContext Context,
string CorrelationId,
DateTimeOffset RequestedAt);
@@ -82,6 +85,9 @@ public interface IModelOperationRequestService
string scopeKey,
string automationMode,
string idempotencyKey,
DateTimeOffset scheduledFor,
string catchUpPolicy,
int maxCatchUp,
string correlationId,
CancellationToken cancellationToken);
}
@@ -15,11 +15,11 @@ public sealed class DapperModelOperationRequestRepository(
insert into evaluation.model_operation_request
(request_id, schedule_id, operation_code, scope_key, automation_mode, idempotency_key,
dataset_id, data_hash, model_version, config_version, code_sha, contract_version,
lifecycle_state, correlation_id, status, requested_at)
lifecycle_state, correlation_id, status, requested_at, scheduled_for)
values
(@RequestId, @ScheduleId, @OperationCode, @ScopeKey, @AutomationMode, @IdempotencyKey,
@DatasetId, @DataHash, @ModelVersion, @ConfigVersion, @CodeSha, @ContractVersion,
@LifecycleState, @CorrelationId, 'REQUESTED', @RequestedAt)
@LifecycleState, @CorrelationId, 'REQUESTED', @RequestedAt, @ScheduledFor)
on conflict (idempotency_key) do nothing;
""";
@@ -47,6 +47,7 @@ public sealed class DapperModelOperationRequestRepository(
request.ScopeKey,
request.AutomationMode,
request.IdempotencyKey,
request.ScheduledFor,
request.Context.VersionSet.DatasetId,
request.Context.VersionSet.DataHash,
request.Context.VersionSet.ModelVersion,
@@ -68,6 +69,9 @@ public sealed class DapperModelOperationRequestRepository(
request.OperationCode,
request.ScopeKey,
request.AutomationMode,
request.ScheduledFor,
request.CatchUpPolicy,
request.MaxCatchUp,
versionSet = request.Context.VersionSet,
boundary = "NO_AUTO_MODEL_MUTATION"
});
@@ -49,7 +49,10 @@ public sealed class ModelOperationsDispatcherJob(
item.OperationCode,
item.ScopeKey,
item.AutomationMode,
idempotencyKey));
idempotencyKey,
occurrence,
item.CatchUpPolicy,
item.MaxCatchUp));
backgroundJobId = jobs.Create(job, new EnqueuedState(item.Queue));
}
@@ -26,7 +26,10 @@ public sealed class ScheduledModelOperationJob(
string operationCode,
string scopeKey,
string automationMode,
string idempotencyKey)
string idempotencyKey,
DateTimeOffset scheduledFor,
string catchUpPolicy,
int maxCatchUp)
{
var correlationId = $"model-operation:{operationCode}:{Guid.NewGuid():N}";
var request = await service.RequestAsync(
@@ -35,6 +38,9 @@ public sealed class ScheduledModelOperationJob(
scopeKey,
automationMode,
idempotencyKey,
scheduledFor,
catchUpPolicy,
maxCatchUp,
correlationId,
CancellationToken.None);
@@ -0,0 +1,56 @@
using KArtSell.BuildingBlocks.Time;
using KArtSell.BuildingBlocks.Versioning;
using KArtSell.Modules.ModelOperations.Application;
namespace KArtSell.ModelOperations.UnitTests;
public sealed class ModelOperationRequestServiceTests
{
[Fact]
public async Task Persists_the_scheduled_occurrence_and_catch_up_provenance()
{
var scheduledFor = new DateTimeOffset(2026, 8, 9, 1, 0, 0, TimeSpan.Zero);
var context = new ApprovedModelContext(
"GLOBAL",
new VersionSet("dataset", "data-hash", "model", "config", "code-sha", "contract"),
"APPROVED",
scheduledFor.AddDays(-1));
var repository = new CapturingRequestRepository();
var service = new ModelOperationRequestService(
new FixedContextReader(context),
repository,
new FixedClock(scheduledFor.AddHours(2)));
var request = await service.RequestAsync(
Guid.NewGuid(), "J31", "GLOBAL", "EVALUATION_ONLY", "J31:GLOBAL:1:20260809010000Z",
scheduledFor, "ALL_WITH_LIMIT", 2, "correlation", CancellationToken.None);
Assert.NotNull(request);
Assert.Same(request, repository.Request);
Assert.Equal(scheduledFor, request.ScheduledFor);
Assert.Equal("ALL_WITH_LIMIT", request.CatchUpPolicy);
Assert.Equal(2, request.MaxCatchUp);
}
private sealed class FixedContextReader(ApprovedModelContext context) : IApprovedModelContextReader
{
public Task<ApprovedModelContext?> ReadAsync(string scopeKey, DateTimeOffset asOf, CancellationToken cancellationToken)
=> Task.FromResult<ApprovedModelContext?>(context);
}
private sealed class CapturingRequestRepository : IModelOperationRequestRepository
{
public ModelOperationRequest? Request { get; private set; }
public Task<bool> AddAsync(ModelOperationRequest request, CancellationToken cancellationToken)
{
Request = request;
return Task.FromResult(true);
}
}
private sealed class FixedClock(DateTimeOffset utcNow) : IClock
{
public DateTimeOffset UtcNow => utcNow;
}
}