security: hard-disable all KIS trading paths (AEG-X-016)

Blocks submit, status, cancel, and settlement before HTTP or database writes and removes the KIS polling recurring job. Evidence: concrete adapter test 1/1 passed with zero HTTP calls. WBS remains IN_PROGRESS pending endpoint/startup override evidence.
This commit is contained in:
2026-08-09 02:30:39 +09:00
parent ec80337389
commit eb59cae8e3
8 changed files with 113 additions and 3 deletions
@@ -0,0 +1,22 @@
# AEG-X-016 — KIS order-submission hard-off
## Scope
- **WBS ID:** `AEG-X-016`; **Requirement / API / DB / Job / UI / Test:** `REQ-KIS-OFF` / `Cross` / `Cross` / `Cross` / `Cross` / `Cross`.
- **Dependency / Gate:** `AEG-X-005` (completed) / `ALL`.
- **Acceptance:** zero external KIS order calls; capability override audit and kill switch pass.
## Source / Assumption / Unknown / Decision Required
- **Source:** `AGENTS.md` hard prohibition on automatic orders/KIS submission; `CapabilityOptions` startup validation; `TradeEndpoints`, `TradeHandlers`, `KisTradeExecutionService`, `TradeStatusPollingJob`, and Program recurring-job registration.
- **Assumption:** the user's instruction is an unconditional hard-off, not a feature flag. Buy, sell, poll, and settlement calls must remain blocked even if an environment value attempts to enable a capability.
- **Unknown:** there is no approved release that authorizes re-enabling KIS. This change supplies no activation path.
- **Decision Required:** a future enablement would require a separately approved release, complete capability/kill-switch audit, and revised WBS evidence; it is out of scope.
## Actual evidence and remaining acceptance
- `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj -c Release --filter FullyQualifiedName~KisTradingHardOffTests --logger "trx;LogFileName=KisTradingHardOffTests_20260809.trx" --results-directory evidence/AEG-X-016`
- Result: passed `1/1`; artifact: `evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx`; SHA-256: `F41D1DF823F91705D322A629B08ADF0BBA03EA1BEFA01DEF47E00C9DAFF2470D`.
- The test invokes submit, status, cancel, and settlement on the concrete KIS adapter and asserts that every call throws the hard-off exception before its HTTP handler is called (zero external calls).
- The submit/poll/settlement handlers guard before any database write or adapter access, and Program removes the historic `trade-status-polling` recurring job.
- **Not complete:** endpoint-level disabled response and a startup configuration-override audit remain to be executed before the full WBS acceptance is claimed.
+1 -1
View File
@@ -182,7 +182,7 @@ AEG-VS-24-04,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,BE,REQ-RPT-
AEG-VS-24-05,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,ASYNC,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Event/Job/Inbox·재처리 구현,"J11R; integration event, JobRun, watermark, inbox","동일 ScopeKey 재실행 결과가 동일하고 중복 side effect 0, transient/business hold/poison이 분리됨",BE/SRE,Data/Ops,5,AEG-VS-24-04,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
AEG-VS-24-06,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,FE,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Vue feature·Zod·Query·컴포넌트 구현,UI-RPT-01; route/api/schema/queries/pages/components,loading/empty/partial/stale/warn/error/401/403/409/expired/readonly와 접근성·권한 경계가 검증됨,FE Lead,UX/QA,6,AEG-VS-24-05,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
AEG-VS-24-07,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,TESTOPS,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,회귀·관제·Runbook·Rollback 증거,T-RPT-001; xUnit/Vitest/Playwright/Data/Replay; OTel/alert/runbook,Golden·integration·failure·replay·E2E와 metric/alert/Owner/Secondary/rollback rehearsal가 Release Evidence에 연결됨,QA/SRE,Module Owner,7,AEG-VS-24-06,G4-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,IN_PROGRESS
AEG-VS-23-01,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,GOV,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,정책·범위·실패상태 계약 확정,"REQ-RECON-001, VS-23 SLICE_SPEC, ADR/Decision Log",사용자 결과 '브로커·내부 주문·체결·보유·현금 break와 승인 정정'·비목표·권한·예외·Source/Assumption/Unknown이 승인됨,PM/Architect,Compliance/Owner,3,"VS-08,VS-16",Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
AEG-VS-23-02,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DATA,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,데이터 시점·스키마·정합성 계약,"MIG-EXE-001/002, DATA_CONTRACT, DQ/lineage 규칙",published_at/revision/valid-time/hash/단위/격리/재처리와 소유자가 정의되고 overwrite 경로가 없음,Data Architect/DBA,Quant/QA,5,AEG-VS-23-01,Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
AEG-VS-23-03,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DOMAIN,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,도메인 불변조건·상태전이 구현,"src/Modules/ExecutionReconciliation/ReconcilePositionsAndFills/Domain, 정책 결정표",순수 정책 테스트에서 우선순위·경계값·단조성·금지 전이가 통과하고 Infrastructure 의존이 없음,BE/Quant Lead,Architect/QA,6,AEG-VS-23-02,Pilot-A,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
182 AEG-VS-24-05 S11 W23-24 VS-24 ReportAndNotify ReportingNotifications ASYNC REQ-RPT-001 RPT-01 MIG-RPT-001 J11R UI-RPT-01 T-RPT-001 Event/Job/Inbox·재처리 구현 J11R; integration event, JobRun, watermark, inbox 동일 ScopeKey 재실행 결과가 동일하고 중복 side effect 0, transient/business hold/poison이 분리됨 BE/SRE Data/Ops 5 AEG-VS-24-04 G4-A SOURCE+DESIGN_PROPOSAL P1 PLANNED
183 AEG-VS-24-06 S11 W23-24 VS-24 ReportAndNotify ReportingNotifications FE REQ-RPT-001 RPT-01 MIG-RPT-001 J11R UI-RPT-01 T-RPT-001 Vue feature·Zod·Query·컴포넌트 구현 UI-RPT-01; route/api/schema/queries/pages/components loading/empty/partial/stale/warn/error/401/403/409/expired/readonly와 접근성·권한 경계가 검증됨 FE Lead UX/QA 6 AEG-VS-24-05 G4-A SOURCE+DESIGN_PROPOSAL P1 PLANNED
184 AEG-VS-24-07 S11 W23-24 VS-24 ReportAndNotify ReportingNotifications TESTOPS REQ-RPT-001 RPT-01 MIG-RPT-001 J11R UI-RPT-01 T-RPT-001 회귀·관제·Runbook·Rollback 증거 T-RPT-001; xUnit/Vitest/Playwright/Data/Replay; OTel/alert/runbook Golden·integration·failure·replay·E2E와 metric/alert/Owner/Secondary/rollback rehearsal가 Release Evidence에 연결됨 QA/SRE Module Owner 7 AEG-VS-24-06 G4-A SOURCE+DESIGN_PROPOSAL P0 PLANNED
185 AEG-X-016 S12 W25-26 Cross Cross-cutting Broker CAPABILITY REQ-KIS-OFF Cross Cross Cross Cross Cross KIS 주문 제출 startup/CI/runtime 차단 고도화 KIS 주문 제출 startup/CI/runtime 차단 외부 주문 호출 0, capability override 감사·kill-switch 통과 Security/Ops Architect/QA 7 AEG-X-005 ALL SOURCE+DESIGN_PROPOSAL P0 PLANNED IN_PROGRESS
186 AEG-VS-23-01 S12 W25-26 VS-23 ReconcilePositionsAndFills ExecutionReconciliation GOV REQ-RECON-001 EXE-01/02,REC-01/02 MIG-EXE-001/002 J12 UI-RECON-01 T-RECON-001 정책·범위·실패상태 계약 확정 REQ-RECON-001, VS-23 SLICE_SPEC, ADR/Decision Log 사용자 결과 '브로커·내부 주문·체결·보유·현금 break와 승인 정정'·비목표·권한·예외·Source/Assumption/Unknown이 승인됨 PM/Architect Compliance/Owner 3 VS-08,VS-16 Pilot-A SOURCE+DESIGN_PROPOSAL P0 PLANNED
187 AEG-VS-23-02 S12 W25-26 VS-23 ReconcilePositionsAndFills ExecutionReconciliation DATA REQ-RECON-001 EXE-01/02,REC-01/02 MIG-EXE-001/002 J12 UI-RECON-01 T-RECON-001 데이터 시점·스키마·정합성 계약 MIG-EXE-001/002, DATA_CONTRACT, DQ/lineage 규칙 published_at/revision/valid-time/hash/단위/격리/재처리와 소유자가 정의되고 overwrite 경로가 없음 Data Architect/DBA Quant/QA 5 AEG-VS-23-01 Pilot-A SOURCE+DESIGN_PROPOSAL P0 PLANNED
188 AEG-VS-23-03 S12 W25-26 VS-23 ReconcilePositionsAndFills ExecutionReconciliation DOMAIN REQ-RECON-001 EXE-01/02,REC-01/02 MIG-EXE-001/002 J12 UI-RECON-01 T-RECON-001 도메인 불변조건·상태전이 구현 src/Modules/ExecutionReconciliation/ReconcilePositionsAndFills/Domain, 정책 결정표 순수 정책 테스트에서 우선순위·경계값·단조성·금지 전이가 통과하고 Infrastructure 의존이 없음 BE/Quant Lead Architect/QA 6 AEG-VS-23-02 Pilot-A SOURCE+DESIGN_PROPOSAL P0 PLANNED
@@ -6,6 +6,7 @@ AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CU
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
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-X-016,S12,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,IN_PROGRESS,TBD,"docs/CURRENT/AEG-X-016_KIS_HARD_OFF_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/TradeExecution/KisTradeExecutionService.cs; src/KArtSell.Modules.ModelOperations/TradeExecution/TradeHandlers.cs; src/KArtSell.Host/Program.cs; tests/KArtSell.Integration.Tests/TradeExecution/KisTradingHardOffTests.cs; evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx",Security/Ops,"User-directed hard-off: concrete KIS submit/status/cancel/settlement adapter throws before HTTP; test proves zero HTTP calls (1/1). Trade handlers guard before DB writes and Program removes trade-status-polling. Remains IN_PROGRESS until endpoint-level disabled response and startup capability-override/kill-switch evidence are run. No KIS activation path was introduced."
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."
1 WBS_ID Sprint Slice_ID Task Status Completion_Date Evidence_Link Owner Notes
6 AEG-X-005 S0 Cross Security auth 고도화 COMPLETED 2026-08-04 docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests) Security/BE ✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'
7 AEG-X-006 S0 Cross Outbox publisher 고도화 COMPLETED 2026-08-04 docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs BE/SRE ✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS.
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-X-016 S12 Cross KIS 주문 제출 startup/CI/runtime 차단 고도화 IN_PROGRESS TBD docs/CURRENT/AEG-X-016_KIS_HARD_OFF_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/TradeExecution/KisTradeExecutionService.cs; src/KArtSell.Modules.ModelOperations/TradeExecution/TradeHandlers.cs; src/KArtSell.Host/Program.cs; tests/KArtSell.Integration.Tests/TradeExecution/KisTradingHardOffTests.cs; evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx Security/Ops User-directed hard-off: concrete KIS submit/status/cancel/settlement adapter throws before HTTP; test proves zero HTTP calls (1/1). Trade handlers guard before DB writes and Program removes trade-status-polling. Remains IN_PROGRESS until endpoint-level disabled response and startup capability-override/kill-switch evidence are run. No KIS activation path was introduced.
10 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.
11 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.
12 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.
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<TestRun id="cccd60ce-1f35-445e-b217-45bdcd0ff494" name="kjh20@KIMJAEHYUN-NOTE 2026-08-09 02:29:26" runUser="KIMJAEHYUN-NOTE\kjh20" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Times creation="2026-08-09T02:29:26.6955415+09:00" queuing="2026-08-09T02:29:26.6955420+09:00" start="2026-08-09T02:29:25.2360804+09:00" finish="2026-08-09T02:29:26.7031426+09:00" />
<TestSettings name="default" id="59b4ac6a-e53e-4388-973c-13a98934a17d">
<Deployment runDeploymentRoot="kjh20_KIMJAEHYUN-NOTE_2026-08-09_02_29_26" />
</TestSettings>
<Results>
<UnitTestResult executionId="eae0eff0-1881-4872-94a4-be93cfb4af12" testId="27c052af-b196-e13a-e07a-4a09b3b4c2de" testName="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests.Every_kis_trade_operation_fails_before_any_http_call" computerName="KIMJAEHYUN-NOTE" duration="00:00:00.0226996" startTime="2026-08-09T02:29:26.5575081+09:00" endTime="2026-08-09T02:29:26.5870108+09:00" testType="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" outcome="Passed" testListId="8c84fa94-04c1-424b-9868-57a2d4851a1d" relativeResultsDirectory="eae0eff0-1881-4872-94a4-be93cfb4af12" />
</Results>
<TestDefinitions>
<UnitTest name="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests.Every_kis_trade_operation_fails_before_any_http_call" storage="d:\jobroomz\kartsell.aegis\tests\kartsell.integration.tests\bin\release\net10.0\kartsell.integration.tests.dll" id="27c052af-b196-e13a-e07a-4a09b3b4c2de">
<Execution id="eae0eff0-1881-4872-94a4-be93cfb4af12" />
<TestMethod codeBase="D:\JobRoomz\KArtSell.Aegis\tests\KArtSell.Integration.Tests\bin\Release\net10.0\KArtSell.Integration.Tests.dll" adapterTypeName="executor://xunit/VsTestRunner3/netcore/" className="KArtSell.Integration.Tests.TradeExecution.KisTradingHardOffTests" name="Every_kis_trade_operation_fails_before_any_http_call" />
</UnitTest>
</TestDefinitions>
<TestEntries>
<TestEntry testId="27c052af-b196-e13a-e07a-4a09b3b4c2de" executionId="eae0eff0-1881-4872-94a4-be93cfb4af12" 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="1" executed="1" passed="1" 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.69] Discovering: KArtSell.Integration.Tests&#xD;
[xUnit.net 00:00:00.77] Discovered: KArtSell.Integration.Tests&#xD;
[xUnit.net 00:00:00.80] Starting: KArtSell.Integration.Tests&#xD;
[xUnit.net 00:00:00.86] Finished: KArtSell.Integration.Tests&#xD;
</StdOut>
</Output>
</ResultSummary>
</TestRun>
+2 -2
View File
@@ -398,8 +398,8 @@ try { RecurringJob.AddOrUpdate<OpenDartDailyBatchJob>("opendart-daily-batch", jo
try { RecurringJob.AddOrUpdate<GenerateDailyRecommendationJob>("daily-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ daily-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ daily-recommendation error"); }
try { RecurringJob.AddOrUpdate<GenerateWeeklyRecommendationJob>("weekly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 * * 6", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ weekly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ weekly-recommendation error"); }
try { RecurringJob.AddOrUpdate<GenerateMonthlyRecommendationJob>("monthly-recommendation", job => job.ExecuteAsync(CancellationToken.None), "0 9 1 * *", new RecurringJobOptions { TimeZone = kstTimeZone }); logger.LogInformation("✅ monthly-recommendation registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ monthly-recommendation error"); }
// DEBT-027: without this, PollTradeStatusHandler/ConfirmSettlementHandler were reachable via DI but never invoked by anything.
try { RecurringJob.AddOrUpdate<KArtSell.Host.Jobs.TradeStatusPollingJob>("trade-status-polling", job => job.ExecuteAsync(CancellationToken.None), "*/2 * * * *", new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc }); logger.LogInformation("✅ trade-status-polling registered"); } catch (Exception ex) { logger.LogWarning(ex, "⚠️ trade-status-polling error"); }
RecurringJob.RemoveIfExists("trade-status-polling");
logger.LogWarning("KIS trade-status-polling removed: KIS trading is hard-disabled.");
logger.LogInformation("🎯 Hangfire background jobs initialization complete");
@@ -25,6 +25,17 @@ public class KisTradeExecutionException : Exception
}
}
public sealed class KisTradingDisabledException : InvalidOperationException
{
public KisTradingDisabledException()
: base("KIS trading is hard-disabled. No buy, sell, cancel, status, or settlement call is permitted.") { }
}
public static class KisTradingHardOff
{
public static void EnsureDisabled() => throw new KisTradingDisabledException();
}
public interface IKisTradeExecutionService
{
Task<(string OrderId, JsonElement Response)> ExecuteTradeAsync(Guid tradeId, int quantity, decimal limitPrice, Guid correlationId, CancellationToken ct = default);
@@ -56,6 +67,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new
{
symbol = "US0100",
@@ -120,6 +132,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var request = new HttpRequestMessage(HttpMethod.Get, $"{KisApiBase}/orders/{kisOrderId}");
request.Headers.Add("X-Correlation-ID", correlationId.ToString());
@@ -159,6 +172,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new { reason = reason };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
@@ -195,6 +209,7 @@ public class KisTradeExecutionService : IKisTradeExecutionService
Guid correlationId,
CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var requestBody = new { confirm = true };
var content = new StringContent(
JsonSerializer.Serialize(requestBody),
@@ -42,6 +42,7 @@ public class SubmitTradeHandler
public async Task<Guid> HandleAsync(SubmitTradeCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = Trade.Create(command.SellDecisionId, command.Quantity, command.CorrelationId, _clock.UtcNow.UtcDateTime);
await _sql.InsertTradeAsync(trade, ct);
_logger.LogInformation("Created trade: {TradeId}", trade.Id);
@@ -127,6 +128,7 @@ public class PollTradeStatusHandler
public async Task HandleAsync(PollTradeStatusCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
if (trade == null)
{
@@ -222,6 +224,7 @@ public class ConfirmSettlementHandler
public async Task HandleAsync(ConfirmSettlementCommand command, CancellationToken ct = default)
{
KisTradingHardOff.EnsureDisabled();
var trade = await _sql.GetTradeByIdAsync(command.TradeId, command.CorrelationId, ct);
if (trade == null)
{
@@ -0,0 +1,35 @@
using System.Net;
using KArtSell.Modules.ModelOperations.TradeExecution;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace KArtSell.Integration.Tests.TradeExecution;
public sealed class KisTradingHardOffTests
{
[Fact]
public async Task Every_kis_trade_operation_fails_before_any_http_call()
{
var handler = new CountingHandler();
var service = new KisTradeExecutionService(new HttpClient(handler), NullLogger<KisTradeExecutionService>.Instance);
var correlationId = Guid.NewGuid();
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.ExecuteTradeAsync(Guid.NewGuid(), 1, 1m, correlationId));
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.GetOrderStatusAsync("order", correlationId));
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.CancelOrderAsync("order", "test", correlationId));
await Assert.ThrowsAsync<KisTradingDisabledException>(() => service.ConfirmSettlementAsync("order", correlationId));
Assert.Equal(0, handler.CallCount);
}
private sealed class CountingHandler : HttpMessageHandler
{
public int CallCount { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
CallCount++;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}
}