From b1ace10b1fd95d67ccbb024a8603cd6c67c56813 Mon Sep 17 00:00:00 2001 From: toki Date: Fri, 22 May 2026 12:47:56 +0900 Subject: [PATCH] update execution result envelope handling --- .../code_review_cloud_G07_0.log | 167 ++++++++++++ .../01_result_contract/complete.log | 34 +++ .../01_result_contract/plan_cloud_G07_0.log | 240 ++++++++++++++++++ .../code_review_cloud_G07_0.log | 176 +++++++++++++ .../02+01_exe_json_cli/complete.log | 39 +++ .../02+01_exe_json_cli/plan_cloud_G07_0.log | 233 +++++++++++++++++ lib/cli/commands/command_exe.dart | 125 +++++++-- lib/oto/application.dart | 13 +- lib/oto/core/build_result.dart | 19 ++ test/oto_application_test.dart | 180 +++++++++++++ 10 files changed, 1197 insertions(+), 29 deletions(-) create mode 100644 agent-task/archive/2026/05/execution_result_envelope/01_result_contract/code_review_cloud_G07_0.log create mode 100644 agent-task/archive/2026/05/execution_result_envelope/01_result_contract/complete.log create mode 100644 agent-task/archive/2026/05/execution_result_envelope/01_result_contract/plan_cloud_G07_0.log create mode 100644 agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/code_review_cloud_G07_0.log create mode 100644 agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/complete.log create mode 100644 agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/plan_cloud_G07_0.log diff --git a/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/code_review_cloud_G07_0.log b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/code_review_cloud_G07_0.log new file mode 100644 index 0000000..7d3e71f --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/code_review_cloud_G07_0.log @@ -0,0 +1,167 @@ + + +# Code Review Reference - RESULT_CONTRACT + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-05-22 +task=execution_result_envelope/01_result_contract, plan=0, tag=RESULT_CONTRACT + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_0.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/2026/05/execution_result_envelope/01_result_contract/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [RESULT_CONTRACT-1] BuildResult JSON Envelope | [x] | +| [RESULT_CONTRACT-2] logEnable False Stdout Suppression | [x] | + +## 구현 체크리스트 + +- [x] `BuildResult`에 안정적인 execution result JSON envelope를 추가한다. +- [x] `Application.build()`와 `Application.log()`가 `logEnable: false`에서 stdout을 오염시키지 않도록 조정한다. +- [x] `test/oto_application_test.dart`에 BuildResult envelope와 log suppression 테스트를 추가한다. +- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/execution_result_envelope/01_result_contract/`를 `agent-task/archive/2026/05/execution_result_envelope/01_result_contract/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/execution_result_envelope/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +- Dart의 내장 `Exception("...")`의 `runtimeType.toString()` 결과가 `'Exception'`이 아닌 `'_Exception'`으로 반환되므로, `test/oto_application_test.dart`의 `BuildResult.toJson` 실패 케이스 검증에서 기대값 타입을 `'_Exception'`으로 조정하여 올바르게 검증이 수행되도록 수정했습니다. 그 외의 사항은 모두 계획과 완벽하게 일치합니다. + +## 주요 설계 결정 + +- `BuildResult`에 `message` getter와 `toJson`을 추가하여 외부 자동화 파이프라인에서 신뢰하고 파싱할 수 있는 규격화된 JSON 형태(schemaVersion, type, success, exitCode, message, error)를 항상 리턴하도록 보장했습니다. +- `Application.build()` 내 catch 절에서 `_logEnable`을 검사하여 예외 발생에 따른 빨강/노랑 콘솔 출력을 선택적으로 suppress했습니다. +- `Application.log()` 및 `printBuildStep()` 내에서 `_logEnable` 조건에 따라 출력을 억제하여, JSON 형태의 output을 온전히 보장하기 위한 stdout 오염 방지 대책을 구현했습니다. + +## 리뷰어를 위한 체크포인트 + +- `BuildResult.toJson()`이 문서화된 stable keys를 항상 유지하는지 확인한다. +- `logEnable: false`가 stdout JSON 순수성을 위한 억제 수단으로 충분한지 확인한다. +- 파일 로그 branch의 기존 error logging 동작을 깨지 않았는지 확인한다. +- 테스트가 성공/실패 envelope와 stdout suppression을 모두 검증하는지 확인한다. + +## 검증 결과 + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### RESULT_CONTRACT-1 중간 검증 +```bash +$ dart test test/oto_application_test.dart --plain-name "BuildResult.toJson" +00:00 +0: loading test/oto_application_test.dart +00:00 +0: BuildResult.toJson exposes success execution envelope +00:00 +1: BuildResult.toJson exposes success execution envelope +00:00 +1: BuildResult.toJson exposes failure execution envelope +00:00 +2: BuildResult.toJson exposes failure execution envelope +00:00 +2: All tests passed! +``` + +### RESULT_CONTRACT-2 중간 검증 +```bash +$ dart test test/oto_application_test.dart --plain-name "logEnable false" +00:00 +0: loading test/oto_application_test.dart +00:00 +0: logEnable false build with logEnable false suppresses failure stdout +00:00 +1: logEnable false build with logEnable false suppresses failure stdout +00:00 +1: logEnable ... with logEnable false suppresses command stdout logs +00:00 +2: logEnable ... with logEnable false suppresses command stdout logs +00:00 +2: All tests passed! +``` + +### 최종 검증 +```bash +$ dart analyze +Analyzing oto... +No issues found! + +$ dart test test/oto_application_test.dart +00:01 +15: CommandExe ... handles missing file without LateInitializationError +00:01 +16: CommandExe ... handles missing file without LateInitializationError +00:01 +16: Application.validateYamlContent ... success contract for valid yaml +00:01 +17: Application.validateYamlContent ... success contract for valid yaml +00:01 +17: ... validateYamlContent returns build yaml failure contract +00:01 +18: ... validateYamlContent returns build yaml failure contract +00:01 +18: ... returns command list failure contract for duplicate id +00:01 +19: ... returns command list failure contract for duplicate id +00:01 +19: ... validateYamlContent returns pipeline failure contract +00:01 +20: ... validateYamlContent returns pipeline failure contract +00:01 +20: ... validateYamlContent exposes stable json shape +00:01 +21: ... validateYamlContent exposes stable json shape +00:01 +21: ... returns failure contract for malformed scheduler section +00:01 +22: ... returns failure contract for malformed scheduler section +00:01 +22: BuildResult.toJson exposes success execution envelope +00:01 +23: BuildResult.toJson exposes success execution envelope +00:01 +23: BuildResult.toJson exposes failure execution envelope +00:01 +24: BuildResult.toJson exposes failure execution envelope +00:01 +24: logEnable false build with logEnable false suppresses failure stdout +00:01 +25: logEnable ... with logEnable false suppresses command stdout logs +00:01 +26: logEnable ... with logEnable false suppresses command stdout logs +00:01 +26: All tests passed! +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Sections and Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` -> `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` -> `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Do not remove | +| 검증 결과 | Implementing agent | Fill actual stdout/stderr | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - correctness: Pass + - completeness: Pass + - test coverage: Pass + - API contract: Pass + - code quality: Pass + - plan deviation: Pass + - verification trust: Pass +- 발견된 문제: 없음 +- 다음 단계: PASS - `complete.log`를 작성하고 task 디렉터리를 archive로 이동한다. diff --git a/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/complete.log b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/complete.log new file mode 100644 index 0000000..40f5cc0 --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/complete.log @@ -0,0 +1,34 @@ +# Complete - execution_result_envelope/01_result_contract + +## 완료 일시 + +2026-05-22 + +## 요약 + +BuildResult execution envelope와 logEnable=false stdout 억제 기반을 구현했고 1회 리뷰 루프에서 PASS로 종료했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | Required/Suggested 이슈 없음 | + +## 구현/정리 내용 + +- `BuildResult`에 stable execution result JSON envelope와 message getter를 추가했다. +- `Application.build()`, `Application.log()`, `printBuildStep()`이 `logEnable: false`에서 stdout을 오염시키지 않도록 조정했다. +- `test/oto_application_test.dart`에 envelope shape와 stdout suppression 테스트를 추가했다. + +## 최종 검증 + +- `dart analyze` - PASS; `No issues found!` +- `dart test test/oto_application_test.dart` - PASS; `All tests passed!` + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/plan_cloud_G07_0.log b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/plan_cloud_G07_0.log new file mode 100644 index 0000000..086ba77 --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/01_result_contract/plan_cloud_G07_0.log @@ -0,0 +1,240 @@ + + +# Execution Result Contract Plan + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제 실행하고, 실제 출력과 구현 메모를 남긴 뒤 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 아카이브와 `complete.log` 작성은 code-review 스킬 전용이다. + +## 배경 + +마일스톤의 다음 남은 항목은 실행 후 성공/실패, exit code, 에러 정보를 외부 자동화가 파싱할 수 있는 envelope로 만드는 것이다. 현재 `BuildResult`는 성공 여부와 exit code를 들고 있지만 JSON 계약이 없다. `exe --json` CLI 작업 전에 core 결과 계약과 로그 억제 기반을 먼저 고정한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/roadmap/current.md` +- `agent-ops/roadmap/milestones/structured-automation-surface.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `agent-ops/rules/project/domain/cli/rules.md` +- `lib/oto/core/build_result.dart` +- `lib/oto/application.dart` +- `lib/cli/cli.dart` +- `lib/oto/commands/util/print.dart` +- `test/oto_application_test.dart` +- `pubspec.yaml` + +### 테스트 커버리지 공백 + +- `BuildResult` JSON shape: 기존 테스트는 `success`, `exitCode`, `error`만 확인하고 `toJson()` 계약을 확인하지 않는다. +- `logEnable: false`의 stdout 억제: 기존 테스트가 없다. `exe --json`이 순수 JSON stdout을 보장하려면 실패 출력과 `Application.log()` 출력 억제를 검증해야 한다. + +### 심볼 참조 + +- renamed/removed symbols: none. +- `BuildResult` 참조: `lib/oto/application.dart`, `lib/cli/commands/command_exe.dart`, `test/oto_application_test.dart`. + +### 분할 판단 + +분할 정책을 먼저 평가했다. 공유 task group은 `execution_result_envelope`이다. + +- `01_result_contract`: core 계약과 `logEnable` 기반을 소유한다. +- `02+01_exe_json_cli`: `01_result_contract` 완료 후 CLI JSON 모드와 bin 스모크를 소유한다. + +core API/foundation 변경과 CLI rollout이 분리되고 검증 방식도 달라서 split gate가 적용된다. + +### 범위 결정 근거 + +- 이 작업은 `BuildResult` JSON 계약과 `Application` 출력 억제 기반만 다룬다. +- `CommandExe` 인자 파싱, isolate 결과 전달, actual bin smoke는 `02+01_exe_json_cli`에서 다룬다. +- step event 계약과 pipeline event 발생 지점은 이번 task group 밖이다. +- 새 패키지는 추가하지 않는다. `dart:convert`, `dart:async`는 SDK 라이브러리다. + +### 빌드 등급 + +- build: `cloud-G07`, review: `cloud-G07`. +- stdout/exit-status 계약이 후속 CLI 자동화 검증의 기반이며, 실제 bin 출력 신뢰성에 영향을 주므로 terminal-agent 성격으로 본다. + +## 구현 체크리스트 + +- [ ] `BuildResult`에 안정적인 execution result JSON envelope를 추가한다. +- [ ] `Application.build()`와 `Application.log()`가 `logEnable: false`에서 stdout을 오염시키지 않도록 조정한다. +- [ ] `test/oto_application_test.dart`에 BuildResult envelope와 log suppression 테스트를 추가한다. +- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 하위 작업은 선행 의존성이 없다. 완료 후 `02+01_exe_json_cli`가 이 작업의 `complete.log`를 전제로 시작한다. + +### [RESULT_CONTRACT-1] BuildResult JSON Envelope + +#### 문제 + +[build_result.dart](/config/workspace/oto/lib/oto/core/build_result.dart:1)는 `success`, `exitCode`, `error`, `stackTrace`만 들고 있고 외부 자동화용 JSON 계약이 없다. + +Before: + +```dart +// lib/oto/core/build_result.dart:1 +class BuildResult { + final bool success; + final int exitCode; + final Object? error; + final StackTrace? stackTrace; + + const BuildResult.success() + : success = true, + exitCode = 0, + error = null, + stackTrace = null; + + const BuildResult.failure(this.error, this.stackTrace, {this.exitCode = 10}) + : success = false; +} +``` + +#### 해결 방법 + +`BuildResult`에 `message` getter와 `toJson()`을 추가한다. schema는 다음 필드를 항상 유지한다. + +```dart +Map toJson() => { + 'schemaVersion': 1, + 'type': 'executionResult', + 'success': success, + 'exitCode': exitCode, + 'message': message, + 'error': success + ? null + : { + 'type': error.runtimeType.toString(), + 'message': message, + }, + }; +``` + +성공 message는 `Build completed successfully.`로 고정하고, 실패 message는 `error?.toString() ?? 'Build failed.'`를 사용한다. stack trace는 자동화 계약의 기본 JSON에 넣지 않는다. + +#### 수정 파일 및 체크리스트 + +- [ ] `lib/oto/core/build_result.dart`: `message` getter 추가. +- [ ] `lib/oto/core/build_result.dart`: `toJson()` 추가. +- [ ] `test/oto_application_test.dart`: `BuildResult` import 추가. +- [ ] `test/oto_application_test.dart`: 성공/failure JSON shape 테스트 추가. + +#### 테스트 작성 + +- 작성: `test/oto_application_test.dart` +- 테스트명: + - `BuildResult.toJson exposes success execution envelope` + - `BuildResult.toJson exposes failure execution envelope` +- 검증 목표: `schemaVersion`, `type`, `success`, `exitCode`, `message`, `error` shape가 안정적인지 확인한다. + +#### 중간 검증 + +```bash +dart test test/oto_application_test.dart --plain-name "BuildResult.toJson" +``` + +기대 결과: 추가한 `BuildResult.toJson` 테스트가 통과한다. + +### [RESULT_CONTRACT-2] logEnable False Stdout Suppression + +#### 문제 + +[application.dart](/config/workspace/oto/lib/oto/application.dart:153)는 `logEnable`과 무관하게 실패 정보를 stdout에 출력한다. [application.dart](/config/workspace/oto/lib/oto/application.dart:180)의 `Application.log()`도 `_logFile == null`이면 `_logEnable`을 보지 않고 `print(message)`를 호출한다. 후속 `exe --json`은 `Application.build(..., logEnable: false)`를 사용해도 stdout이 JSON 외 텍스트로 오염될 수 있다. + +Before: + +```dart +// lib/oto/application.dart:153 +} catch (e, stacktrace) { + await CLI.printString(e.toString(), color: Color.redStrong); + await CLI.printString(stacktrace.toString(), color: Color.yellowStrong); + await printBuildStep('Build Failed', Color.redStrong); + return BuildResult.failure(e, stacktrace); +} +``` + +```dart +// lib/oto/application.dart:180 +static void log(String message, {LogType logType = LogType.verbose}) { + if (_logFile == null) { + print(message); + } else { + if (_logEnable || logType == LogType.error) { + _logFile?.writeAsStringSync('[${getDates()}] $message\n', + mode: FileMode.append, flush: true); + } + } +} +``` + +#### 해결 방법 + +`Application.build()`의 catch 출력은 `_logEnable`이 true일 때만 수행한다. `Application.log()`의 stdout branch도 `_logEnable`이 true일 때만 `print(message)`를 호출한다. 파일 로그 branch의 기존 error logging 동작은 유지한다. + +```dart +} catch (e, stacktrace) { + if (_logEnable) { + await CLI.printString(e.toString(), color: Color.redStrong); + await CLI.printString(stacktrace.toString(), color: Color.yellowStrong); + await printBuildStep('Build Failed', Color.redStrong); + } + return BuildResult.failure(e, stacktrace); +} +``` + +```dart +if (_logFile == null) { + if (_logEnable) { + print(message); + } +} else { + ... +} +``` + +#### 수정 파일 및 체크리스트 + +- [ ] `lib/oto/application.dart`: catch 출력에 `_logEnable` 조건 추가. +- [ ] `lib/oto/application.dart`: stdout branch의 `Application.log()`가 `_logEnable`을 존중하도록 수정. +- [ ] `test/oto_application_test.dart`: `dart:async` import 추가. +- [ ] `test/oto_application_test.dart`: `runZoned`로 `print`를 캡처해 `logEnable: false`에서 출력이 없는지 테스트. + +#### 테스트 작성 + +- 작성: `test/oto_application_test.dart` +- 테스트명: + - `build with logEnable false suppresses failure stdout` + - `build with logEnable false suppresses command stdout logs` +- 검증 목표: 실패 출력과 `PrintCommand`의 `Application.log()` 출력이 `logEnable: false`에서 stdout으로 나오지 않는지 확인한다. + +#### 중간 검증 + +```bash +dart test test/oto_application_test.dart --plain-name "logEnable false" +``` + +기대 결과: `logEnable false` 관련 테스트가 통과한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `lib/oto/core/build_result.dart` | RESULT_CONTRACT-1 | +| `lib/oto/application.dart` | RESULT_CONTRACT-2 | +| `test/oto_application_test.dart` | RESULT_CONTRACT-1, RESULT_CONTRACT-2 | + +## 최종 검증 + +```bash +dart analyze +dart test test/oto_application_test.dart +``` + +기대 결과: analyze issue 없이 통과하고 `test/oto_application_test.dart` 전체가 통과한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/code_review_cloud_G07_0.log b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/code_review_cloud_G07_0.log new file mode 100644 index 0000000..449aae9 --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/code_review_cloud_G07_0.log @@ -0,0 +1,176 @@ + + +# Code Review Reference - EXE_JSON + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every implementation-owned section below is filled in. +> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving. +> Fill implementation-owned sections, then stop with active files in place and report ready for review. +> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-05-22 +task=execution_result_envelope/02+01_exe_json_cli, plan=0, tag=EXE_JSON + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다. + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다. + +1. 판정을 append한다. +2. `CODE_REVIEW-cloud-G07.md` -> `code_review_cloud_G07_0.log`, `PLAN-cloud-G07.md` -> `plan_cloud_G07_0.log`로 아카이브한다. +3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/`로 이동한다. WARN/FAIL이면 다음 active plan/review 파일을 즉시 작성한다. +4. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [EXE_JSON-1] CommandExe JSON Mode And Result Transport | [x] | +| [EXE_JSON-2] Actual Bin Pure JSON Smoke | [x] | + +## 구현 체크리스트 + +- [x] `01_result_contract`의 `complete.log`가 존재하는지 확인하고 시작한다. +- [x] `CommandExe` 인자 파싱을 `--json` 위치와 무관하게 동작하도록 정리한다. +- [x] isolate complete 이벤트가 `BuildResult.toJson()` envelope를 전달하고 기존 scheduler 경로를 보존하도록 처리한다. +- [x] `oto exe -f --json`이 stdout에 JSON만 출력하고 process exit code를 envelope와 맞추도록 구현한다. +- [x] `test/oto_application_test.dart`에 CommandExe JSON unit 및 actual bin smoke 테스트를 추가한다. +- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. +> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_0.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_0.log`로 아카이브한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/execution_result_envelope/02+01_exe_json_cli/`를 `agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/execution_result_envelope/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [ ] WARN/FAIL이면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G07.md`를 작성하고 `complete.log`를 작성하지 않는다. + +## 계획 대비 변경 사항 + +계획과 완전히 일치하게 구현하였으며, 특이한 대안 경로나 변경 사항은 없습니다. + +## 주요 설계 결정 + +1. **`CommandExe`의 파싱 루프 선파싱/후실행 분리**: 기존의 인자 매칭 시 즉시 실행 후 브레이크하던 흐름을, `--json` 모드 지원을 위해 유연한 `for` 루프 매치 구조로 개선하였습니다. `--json`의 위치가 앞이나 뒤 어디든 올바르게 파싱됩니다. +2. **`isComplete()`에서 `Map`과 `int` 수신 호환**: 스케줄러 등 기존 동작을 깨뜨리지 않고, 새 JSON 모드에서는 `BuildResult.toJson()` 결과를 받아 JSON stdout 처리를 할 수 있도록 `data is Map`과 `data is int`를 모두 지원하도록 설계하였습니다. +3. **`_printString` 생성자 주입 지원**: 테스팅 및 샌드박싱 환경에서 `stdout` 출력을 낚아채어 검증할 수 있도록 `CommandExe` 생성자에서 `_printString` 함수를 옵셔널로 주입받는 패턴을 유지하였습니다. + +## 리뷰어를 위한 체크포인트 + +- 구현 시작 전에 `01_result_contract` 완료 근거를 확인했는지 본다. +- `CommandExe`가 `--json` 위치와 무관하게 파싱되는지 확인한다. +- complete 이벤트가 Map envelope와 기존 int 둘 다 처리해 scheduler 경로를 깨지 않는지 확인한다. +- actual bin stdout이 JSON만 포함하고 exit code가 envelope와 일치하는지 확인한다. + +## 검증 결과 + +각 검증 단계를 실제로 실행하여 아래와 같은 정상 패스 로그를 확인하였습니다. + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### EXE_JSON-1 중간 검증 +```bash +$ dart test test/oto_application_test.dart --plain-name "CommandExe" +00:00 +0: loading test/oto_application_test.dart +00:00 +0: CommandExe ... handles missing file without LateInitializationError +00:00 +0: CommandExe exe -f handles missing file without LateInitializationError +There are no files in path /tmp/oto_missing_1779415634176676.yaml + +00:00 +1: CommandExe ... handles missing file without LateInitializationError +00:00 +1: CommandExe ... shouldPrintExecuteLog suppresses execute log for json +00:00 +2: CommandExe ... shouldPrintExecuteLog suppresses execute log for json +00:00 +2: CommandExe ... -f missing file --json returns execution result json +00:00 +3: CommandExe ... -f missing file --json returns execution result json +00:00 +3: CommandExe ... parseable execution result json and no execute logs +00:01 +3: CommandExe ... parseable execution result json and no execute logs +00:02 +3: CommandExe ... parseable execution result json and no execute logs +00:03 +3: CommandExe ... parseable execution result json and no execute logs +00:03 +4: CommandExe ... parseable execution result json and no execute logs +00:03 +4: All tests passed! +``` + +### EXE_JSON-2 중간 검증 +```bash +$ dart test test/oto_application_test.dart --plain-name "actual bin exe --json" +00:00 +0: loading test/oto_application_test.dart +00:00 +0: CommandExe ... parseable execution result json and no execute logs +00:01 +0: CommandExe ... parseable execution result json and no execute logs +00:02 +0: CommandExe ... parseable execution result json and no execute logs +00:02 +1: CommandExe ... parseable execution result json and no execute logs +00:02 +1: All tests passed! +``` + +### 최종 검증 +```bash +$ dart analyze +Analyzing oto... +No issues found! + +$ dart test test/oto_application_test.dart +00:01 +29: All tests passed! + +$ dart test test/oto_validate_cli_test.dart test/oto_catalog_cli_test.dart test/oto_scheduler_runtime_test.dart +00:05 +19: All tests passed! +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. +> Leave review-agent-only sections unchanged. + +## Sections and Ownership + +| Section | Owner | Note | +|---------|-------|------| +| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these | +| 구현 항목별 완료 여부 | Implementing agent | Check `[ ]` -> `[x]` only | +| 구현 체크리스트 | Implementing agent | Check `[ ]` -> `[x]` only | +| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify | +| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text | +| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Do not remove | +| 검증 결과 | Implementing agent | Fill actual stdout/stderr | + +## 코드리뷰 결과 + +### 종합 판정 + +PASS + +### 차원별 평가 + +| 차원 | 평가 | 메모 | +|------|------|------| +| Correctness | Pass | `--json` 위치 독립 파싱, JSON envelope 출력, exit code 전파가 실제 bin smoke와 missing-file smoke에서 확인됨. | +| Completeness | Pass | 계획/리뷰 체크리스트가 일치하고 구현 소유 항목이 모두 완료됨. | +| Test coverage | Pass | CommandExe unit, actual bin JSON smoke, 관련 CLI/scheduler 회귀 테스트가 있음. | +| API contract | Pass | isolate complete 이벤트가 Map envelope와 기존 int를 모두 수신하며 scheduler 호출 경로는 보존됨. | +| Code quality | Pass | 디버그 출력/TODO/죽은 코드는 없고, EOF 여분 빈 줄만 Nit로 기록함. | +| Plan deviation | Pass | 선행 `01_result_contract` 변경과 현재 CLI 연결 변경이 계획된 의존 관계 안에 있음. | +| Verification trust | Pass | 기록된 검증 명령을 재실행했고, 추가 actual bin missing-file JSON smoke도 exit code 10/envelope 일치. | + +### 발견된 문제 + +- Nit: `lib/cli/commands/command_exe.dart:215` 파일 끝에 여분 빈 줄이 있어 `git diff --check`가 `new blank line at EOF`를 보고합니다. 마지막 빈 줄을 하나로 정리하면 됩니다. +- Nit: `lib/oto/core/build_result.dart:34` 파일 끝에 여분 빈 줄이 있어 `git diff --check`가 `new blank line at EOF`를 보고합니다. 마지막 빈 줄을 하나로 정리하면 됩니다. + +### 다음 단계 + +PASS: `complete.log`를 작성하고 active task 디렉터리를 archive로 이동한다. diff --git a/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/complete.log b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/complete.log new file mode 100644 index 0000000..c133fe9 --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/complete.log @@ -0,0 +1,39 @@ +# Complete - execution_result_envelope/02+01_exe_json_cli + +## 완료 일시 + +2026-05-22 + +## 요약 + +`oto exe -f --json` 실행 결과 envelope 연결 작업을 1회 리뷰 루프로 완료했으며 최종 판정은 PASS입니다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | JSON stdout, process exit code, scheduler 호환 수신 경로가 검증됨. | + +## 구현/정리 내용 + +- `CommandExe` 인자 파싱을 `--json` 위치와 무관한 선파싱/후실행 구조로 정리하고, JSON 모드에서 `BuildResult.toJson()` envelope를 stdout에 출력하도록 연결함. +- isolate complete 이벤트가 Map envelope와 기존 int를 모두 처리하도록 하여 scheduler 호출 경로를 보존함. +- 실제 `dart run bin/main.dart exe -f --json` smoke와 CommandExe JSON unit 테스트를 추가함. + +## 최종 검증 + +- `dart analyze` - PASS; `No issues found!` +- `dart test test/oto_application_test.dart --plain-name "CommandExe"` - PASS; `All tests passed!` +- `dart test test/oto_application_test.dart --plain-name "actual bin exe --json"` - PASS; `All tests passed!` +- `dart test test/oto_application_test.dart` - PASS; `00:03 +29: All tests passed!` +- `dart test test/oto_validate_cli_test.dart test/oto_catalog_cli_test.dart test/oto_scheduler_runtime_test.dart` - PASS; `00:05 +19: All tests passed!` +- `dart run bin/main.dart exe -f --json` - PASS; process exit code 10 and JSON envelope `exitCode: 10` matched. + +## 잔여 Nit + +- `lib/cli/commands/command_exe.dart:215` EOF 여분 빈 줄 때문에 `git diff --check`가 `new blank line at EOF`를 보고함. +- `lib/oto/core/build_result.dart:34` EOF 여분 빈 줄 때문에 `git diff --check`가 `new blank line at EOF`를 보고함. + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/plan_cloud_G07_0.log b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/plan_cloud_G07_0.log new file mode 100644 index 0000000..f5a59e7 --- /dev/null +++ b/agent-task/archive/2026/05/execution_result_envelope/02+01_exe_json_cli/plan_cloud_G07_0.log @@ -0,0 +1,233 @@ + + +# Exe JSON CLI Plan + +## 이 파일을 읽는 구현 에이전트에게 + +구현 완료 전 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제 실행하고, 실제 출력과 구현 메모를 남긴 뒤 active 파일은 그대로 둔 채 리뷰 준비를 보고한다. 최종 아카이브와 `complete.log` 작성은 code-review 스킬 전용이다. + +## 배경 + +`BuildResult` envelope가 생기면 기존 `oto exe` 실행 결과를 외부 자동화가 파싱할 수 있는 CLI 출력으로 연결해야 한다. 현재 `CommandExe`는 exit code만 isolate complete 이벤트로 받고, `--json` 옵션이 없으며, `-f`를 만나면 즉시 실행하고 나머지 인자를 무시한다. 이 작업은 `oto exe -f --json`을 안정적인 JSON stdout과 exit code 계약으로 만든다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/roadmap/current.md` +- `agent-ops/roadmap/milestones/structured-automation-surface.md` +- `agent-ops/rules/project/domain/cli/rules.md` +- `agent-ops/rules/project/domain/core/rules.md` +- `lib/cli/commands/command_exe.dart` +- `lib/cli/commands/command_base.dart` +- `lib/cli/commands/command_manager.dart` +- `lib/cli/commands/command_validate.dart` +- `lib/cli/commands/command_catalog.dart` +- `lib/cli/cli.dart` +- `lib/cli/commands/scheduler/scheduler_isolate.dart` +- `lib/oto/core/build_result.dart` +- `lib/oto/application.dart` +- `bin/main.dart` +- `test/oto_application_test.dart` +- `test/oto_validate_cli_test.dart` +- `test/oto_catalog_cli_test.dart` +- `test/oto_scheduler_runtime_test.dart` +- `pubspec.yaml` + +### 테스트 커버리지 공백 + +- `CommandExe.shouldPrintExecuteLog(['--json'])`: 기존 테스트 없음. +- `CommandExe` direct JSON output: 기존 테스트 없음. +- actual bin `dart run bin/main.dart exe -f --json`: 기존 테스트 없음. +- missing file JSON envelope와 exit code: 기존 테스트는 missing file이 LateInitializationError 없이 끝나는지만 확인한다. + +### 심볼 참조 + +- renamed/removed symbols: none. +- `CommandExe` 참조: `bin/main.dart`, `lib/cli/commands/scheduler/scheduler_isolate.dart`, `test/oto_application_test.dart`. +- `IsolateExe` complete event data: [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:73)에서 int만 처리하고, [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:138)에서 `result.exitCode`만 보낸다. + +### 분할 판단 + +분할 정책을 먼저 평가했다. 공유 task group은 `execution_result_envelope`이다. + +- `01_result_contract`: 선행 작업. `BuildResult.toJson()`과 `logEnable` stdout 억제 기반을 제공해야 한다. +- `02+01_exe_json_cli`: 이 작업. 디렉터리명 기준으로 `01_result_contract`의 `complete.log`가 선행 조건이다. + +CLI 인자 파싱, isolate 결과 전달, actual bin smoke는 core 계약과 독립 검증 단위가 다르므로 별도 plan으로 둔다. + +### 범위 결정 근거 + +- 이 작업은 `oto exe -f --json`을 우선 검증 대상으로 한다. +- `-j`, `-t` 기존 실행 경로는 깨지지 않게 유지하되 별도 bin smoke는 추가하지 않는다. +- scheduler 내부 실행은 JSON 출력 대상이 아니므로 `executeScheduler()` 동작을 보존한다. +- step event, log stream, artifact event는 이번 plan 밖이다. +- 새 패키지는 추가하지 않는다. JSON encoding은 `dart:convert`를 사용한다. + +### 빌드 등급 + +- build: `cloud-G07`, review: `cloud-G07`. +- terminal CLI workflow, stdout parsing, exit-status contract, actual bin smoke가 성공 조건이라 `cloud-G07` 이상이 필요하다. + +## 구현 체크리스트 + +- [ ] `01_result_contract`의 `complete.log`가 존재하는지 확인하고 시작한다. +- [ ] `CommandExe` 인자 파싱을 `--json` 위치와 무관하게 동작하도록 정리한다. +- [ ] isolate complete 이벤트가 `BuildResult.toJson()` envelope를 전달하고 기존 scheduler 경로를 보존하도록 처리한다. +- [ ] `oto exe -f --json`이 stdout에 JSON만 출력하고 process exit code를 envelope와 맞추도록 구현한다. +- [ ] `test/oto_application_test.dart`에 CommandExe JSON unit 및 actual bin smoke 테스트를 추가한다. +- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 review stub에 기록한다. +- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 의존 관계 및 구현 순서 + +이 하위 작업은 `01_result_contract`에 의존한다. 구현 시작 전 `agent-task/execution_result_envelope/01_result_contract/complete.log` 또는 archive된 PASS 결과가 있어야 한다. + +### [EXE_JSON-1] CommandExe JSON Mode And Result Transport + +#### 문제 + +[command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:30)의 `execute()`는 인자를 순회하다가 `-j`, `-t`, `-f`를 만나면 즉시 실행 후 `break`한다. 따라서 `exe -f file.yaml --json` 같은 후행 옵션을 해석할 수 없다. 또한 [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:73)는 complete 이벤트 data가 int인 경우만 처리하고, [command_exe.dart](/config/workspace/oto/lib/cli/commands/command_exe.dart:138)는 isolate에서 `result.exitCode`만 보낸다. + +Before: + +```dart +// lib/cli/commands/command_exe.dart:30 +Future execute(List parameters) async { + for (var i = 0; i < parameters.length; ++i) { + var item = parameters[i]; + if (item.startsWith('-j')) { + _handler = IsolateManager.create(IsolateExe(BuildType.jenkins)); + await isComplete(); + break; + } else if (item.startsWith('-t')) { + _handler = IsolateManager.create(IsolateExe(BuildType.test)); + await isComplete(); + break; + } else if (item.startsWith('-f')) { + var file = ''; + if (parameters.length > i + 1) { + file = parameters[i + 1]; + } else { + file = item.replaceFirst('-f', '').trimLeft(); + } + await startYaml(file); + break; + } else { + printHelp(); + } + } + exitCode = _exitCode; + _handler?.exit(); + return simpleFuture; +} +``` + +#### 해결 방법 + +`execute()`를 먼저 파싱하고 나중에 실행하는 구조로 바꾼다. `--json`, `--file`, `--file=`, `-f `, `-fpath`를 지원하고, JSON 모드에서는 `shouldPrintExecuteLog()`가 false를 반환한다. + +complete 이벤트는 기존 int와 새 Map envelope를 모두 처리한다. + +```dart +_handler!.addListener('complete', (data) { + if (data is Map) { + _resultJson = Map.from(data); + _exitCode = (_resultJson!['exitCode'] as num?)?.toInt() ?? 10; + } else if (data is int) { + _exitCode = data; + } + _complete = true; +}); +``` + +`IsolateExe.onReady()`는 `result.toJson()`을 보낸다. `executeScheduler()`는 JSON을 출력하지 않고 기존처럼 완료만 기다리도록 유지한다. + +#### 수정 파일 및 체크리스트 + +- [ ] `lib/cli/commands/command_exe.dart`: `dart:convert`와 `BuildResult` import 추가. +- [ ] `lib/cli/commands/command_exe.dart`: `_resultJson`과 optional `_printString` 추가. +- [ ] `lib/cli/commands/command_exe.dart`: `arguments`에 `--json`, `--file` 설명 추가. +- [ ] `lib/cli/commands/command_exe.dart`: `shouldPrintExecuteLog()` override 추가. +- [ ] `lib/cli/commands/command_exe.dart`: 인자 파싱을 선파싱/후실행 구조로 변경. +- [ ] `lib/cli/commands/command_exe.dart`: complete listener가 Map envelope와 int를 모두 처리하도록 변경. +- [ ] `lib/cli/commands/command_exe.dart`: `IsolateExe.onReady()`에서 `result.toJson()` 전송. + +#### 테스트 작성 + +- 작성: `test/oto_application_test.dart` +- 테스트명: + - `CommandExe shouldPrintExecuteLog suppresses execute log for json` + - `CommandExe exe -f missing file --json returns execution result json` +- 검증 목표: JSON 옵션이 위치와 무관하게 인식되고, missing file에서도 JSON envelope와 exit code 10이 나온다. + +#### 중간 검증 + +```bash +dart test test/oto_application_test.dart --plain-name "CommandExe" +``` + +기대 결과: 기존 missing file 테스트와 새 CommandExe JSON 테스트가 통과한다. + +### [EXE_JSON-2] Actual Bin Pure JSON Smoke + +#### 문제 + +마일스톤 완료 기준은 외부 자동화가 내부 Dart API가 아니라 CLI를 호출해 결과를 해석하는 것이다. [command_manager.dart](/config/workspace/oto/lib/cli/commands/command_manager.dart:41)는 기본적으로 `Execute command:` 로그를 출력하고, [command_validate.dart](/config/workspace/oto/lib/cli/commands/command_validate.dart:32)와 [command_catalog.dart](/config/workspace/oto/lib/cli/commands/command_catalog.dart:32)는 JSON 모드에서 이를 끄는 패턴을 이미 갖고 있다. `exe --json`도 actual bin smoke로 stdout이 parseable JSON인지 검증해야 한다. + +Before: + +```dart +// lib/cli/commands/command_manager.dart:41 +if (commandItem.shouldPrintExecuteLog(parameters)) { + await CLI.println('Execute command: $command$argStr', + color: Color.green); +} +``` + +#### 해결 방법 + +`CommandExe.shouldPrintExecuteLog()`를 `CommandCatalogCli`/`CommandValidateCli`와 같은 패턴으로 구현한다. JSON 모드 실행은 `Application.build(..., logEnable: false)` 기반으로 돌리고, 완료 후 `JsonEncoder.withIndent(' ')`로 envelope를 한 번만 stdout에 쓴다. + +`01_result_contract`에서 `Application.log()`가 `_logEnable`을 존중해야 `PrintCommand`가 있는 YAML도 stdout을 오염시키지 않는다. + +#### 수정 파일 및 체크리스트 + +- [ ] `lib/cli/commands/command_exe.dart`: JSON 모드에서 `IsolateExe`에 `logEnable: false` 전달. +- [ ] `lib/cli/commands/command_exe.dart`: 실행 완료 후 `_resultJson`을 stdout에 출력. +- [ ] `test/oto_application_test.dart`: 실제 `dart run bin/main.dart exe -f --json` 테스트 추가. +- [ ] `test/oto_application_test.dart`: stdout에 `Execute command:`, `Usage:`, YAML command output이 섞이지 않는지 확인. + +#### 테스트 작성 + +- 작성: `test/oto_application_test.dart` +- 테스트명: `actual bin exe --json output has parseable execution result json and no execute logs` +- fixture: temp YAML에 `Print` command를 넣어 `Application.log()` 억제까지 검증한다. +- assertion: process exit code 0, stdout JSON parse 성공, `type == executionResult`, `success == true`, `exitCode == 0`, stdout에 `Execute command:`, `Usage:`, YAML message가 없음. + +#### 중간 검증 + +```bash +dart test test/oto_application_test.dart --plain-name "actual bin exe --json" +``` + +기대 결과: actual bin JSON smoke가 통과한다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `lib/cli/commands/command_exe.dart` | EXE_JSON-1, EXE_JSON-2 | +| `test/oto_application_test.dart` | EXE_JSON-1, EXE_JSON-2 | + +## 최종 검증 + +```bash +dart analyze +dart test test/oto_application_test.dart +dart test test/oto_validate_cli_test.dart test/oto_catalog_cli_test.dart test/oto_scheduler_runtime_test.dart +``` + +기대 결과: analyze issue 없이 통과하고 관련 CLI/application/scheduler 회귀 테스트가 모두 통과한다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/lib/cli/commands/command_exe.dart b/lib/cli/commands/command_exe.dart index 2fe43fe..f28f13c 100644 --- a/lib/cli/commands/command_exe.dart +++ b/lib/cli/commands/command_exe.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_init_to_null import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:dart_framework/data/isolate_data.dart'; @@ -9,6 +10,7 @@ import 'package:oto/cli/cli.dart'; import 'package:oto/cli/commands/command_base.dart'; import 'package:dart_framework/utils/system_util.dart'; import 'package:oto/oto/application.dart'; +import 'package:oto/oto/core/build_result.dart'; import 'package:oto/oto/data/command_data.dart'; class CommandExe extends CommandBase { @@ -16,41 +18,99 @@ class CommandExe extends CommandBase { bool _complete = false; bool _ready = false; int _exitCode = 0; + Map? _resultJson; + final Future Function(String) _printString; + + CommandExe({Future Function(String)? printString}) + : _printString = printString ?? + ((value) async { + stdout.writeln(value); + }); @override Map> get arguments => { '-j': ["Used when it's a Jenkins build."], '-t': ["This is the setup for testing."], - '-f {file path}': ["Run the yaml file to process."] + '-f {file path}': ["Run the yaml file to process."], + '--file {file path}': ["Run the yaml file to process."], + '--json': ["Output execution result as structured JSON format."] }; @override String get name => 'exe'; + @override + bool shouldPrintExecuteLog(List parameters) { + return !parameters.contains('--json'); + } + @override Future execute(List parameters) async { - for (var i = 0; i < parameters.length; ++i) { - var item = parameters[i]; - if (item.startsWith('-j')) { - _handler = IsolateManager.create(IsolateExe(BuildType.jenkins)); - await isComplete(); - break; - } else if (item.startsWith('-t')) { - _handler = IsolateManager.create(IsolateExe(BuildType.test)); - await isComplete(); - break; - } else if (item.startsWith('-f')) { - var file = ''; - if (parameters.length > i + 1) { - file = parameters[i + 1]; + BuildType? buildType; + String? filePath; + bool isJson = false; + bool hasUnknownOption = false; + + if (parameters.isEmpty) { + printHelp(); + return simpleFuture; + } + + for (int i = 0; i < parameters.length; i++) { + final param = parameters[i]; + if (param == '--json') { + isJson = true; + } else if (param == '-j') { + buildType = BuildType.jenkins; + } else if (param == '-t') { + buildType = BuildType.test; + } else if (param.startsWith('-f')) { + buildType = BuildType.file; + if (param == '-f') { + if (i + 1 < parameters.length) { + filePath = parameters[++i]; + } else { + hasUnknownOption = true; + } } else { - file = item.replaceFirst('-f', '').trimLeft(); + filePath = param.substring(2); + } + } else if (param.startsWith('--file=')) { + buildType = BuildType.file; + filePath = param.substring('--file='.length); + } else if (param == '--file') { + buildType = BuildType.file; + if (i + 1 < parameters.length) { + filePath = parameters[++i]; + } else { + hasUnknownOption = true; } - await startYaml(file); - break; } else { - printHelp(); + hasUnknownOption = true; } } + + if (hasUnknownOption || buildType == null) { + printHelp(); + return simpleFuture; + } + + final logEnable = !isJson; + + if (buildType == BuildType.file) { + if (filePath == null || filePath.isEmpty) { + printHelp(); + return simpleFuture; + } + await startYaml(filePath, logEnable: logEnable); + } else { + _handler = IsolateManager.create(IsolateExe(buildType, logEnable: logEnable)); + await isComplete(); + } + + if (isJson && _resultJson != null) { + await _printString(const JsonEncoder.withIndent(' ').convert(_resultJson)); + } + exitCode = _exitCode; _handler?.exit(); return simpleFuture; @@ -69,9 +129,13 @@ class CommandExe extends CommandBase { _ready = false; _complete = false; _exitCode = 0; + _resultJson = null; _handler!.addEventListener(IsolateEvent.ready, (data) => _ready = true); _handler!.addListener('complete', (data) { - if (data is int) { + if (data is Map) { + _resultJson = Map.from(data); + _exitCode = (_resultJson!['exitCode'] as num?)?.toInt() ?? 10; + } else if (data is int) { _exitCode = data; } _complete = true; @@ -82,18 +146,28 @@ class CommandExe extends CommandBase { return simpleFuture; } - Future startYaml(String target) async { + Future startYaml(String target, {bool logEnable = true}) async { var file = File(target); if (file.existsSync()) { _handler = IsolateManager.create(IsolateExe(BuildType.file, + logEnable: logEnable, yamlContent: file.readAsStringSync( /*encoding: Platform.isWindows ? eucKr : systemEncoding*/))); await isComplete(); } else { - await CLI.print([ - CLI.style('There are no files in path "$target"', color: Color.red) - ]); _exitCode = 10; + if (!logEnable) { + final failureResult = BuildResult.failure( + 'There are no files in path "$target"', + null, + exitCode: 10, + ); + _resultJson = failureResult.toJson(); + } else { + await CLI.print([ + CLI.style('There are no files in path "$target"', color: Color.red) + ]); + } } return simpleFuture; } @@ -135,6 +209,7 @@ class IsolateExe extends IsolateBase { buildData: _build, logEnable: _logEnable, logPath: _logPath); - send(IsoMessege('complete', data: result.exitCode)); + send(IsoMessege('complete', data: result.toJson())); } } + diff --git a/lib/oto/application.dart b/lib/oto/application.dart index a137577..247f06d 100644 --- a/lib/oto/application.dart +++ b/lib/oto/application.dart @@ -151,9 +151,11 @@ class Application { //start build await pipeline.execute(); } catch (e, stacktrace) { - await CLI.printString(e.toString(), color: Color.redStrong); - await CLI.printString(stacktrace.toString(), color: Color.yellowStrong); - await printBuildStep('Build Failed', Color.redStrong); + if (_logEnable) { + await CLI.printString(e.toString(), color: Color.redStrong); + await CLI.printString(stacktrace.toString(), color: Color.yellowStrong); + await printBuildStep('Build Failed', Color.redStrong); + } return BuildResult.failure(e, stacktrace); } @@ -179,7 +181,9 @@ class Application { static void log(String message, {LogType logType = LogType.verbose}) { if (_logFile == null) { - print(message); + if (_logEnable) { + print(message); + } } else { if (_logEnable || logType == LogType.error) { _logFile?.writeAsStringSync('[${getDates()}] $message\n', @@ -287,6 +291,7 @@ class Application { } Future printBuildStep(String name, Color? color) async { + if (!_logEnable) return simpleFuture; var message = '''$enter********************************************************************************************* * $name diff --git a/lib/oto/core/build_result.dart b/lib/oto/core/build_result.dart index 5e008ac..ff06e12 100644 --- a/lib/oto/core/build_result.dart +++ b/lib/oto/core/build_result.dart @@ -12,4 +12,23 @@ class BuildResult { const BuildResult.failure(this.error, this.stackTrace, {this.exitCode = 10}) : success = false; + + String get message => success + ? 'Build completed successfully.' + : (error?.toString() ?? 'Build failed.'); + + Map toJson() => { + 'schemaVersion': 1, + 'type': 'executionResult', + 'success': success, + 'exitCode': exitCode, + 'message': message, + 'error': success + ? null + : { + 'type': error.runtimeType.toString(), + 'message': message, + }, + }; } + diff --git a/test/oto_application_test.dart b/test/oto_application_test.dart index 057adb9..b8773a1 100644 --- a/test/oto_application_test.dart +++ b/test/oto_application_test.dart @@ -1,7 +1,10 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:oto/cli/commands/command_exe.dart'; import 'package:oto/oto/application.dart'; +import 'package:oto/oto/core/build_result.dart'; import 'package:test/test.dart'; void main() { @@ -450,4 +453,181 @@ scheduler: "not_a_map" expect(result.message, isNotEmpty); }); }); + + group('BuildResult.toJson', () { + test('exposes success execution envelope', () { + const result = BuildResult.success(); + final json = result.toJson(); + + expect(json['schemaVersion'], 1); + expect(json['type'], 'executionResult'); + expect(json['success'], isTrue); + expect(json['exitCode'], 0); + expect(json['message'], 'Build completed successfully.'); + expect(json['error'], isNull); + }); + + test('exposes failure execution envelope', () { + final error = Exception('Some critical error'); + final stack = StackTrace.current; + final result = BuildResult.failure(error, stack, exitCode: 15); + final json = result.toJson(); + + expect(json['schemaVersion'], 1); + expect(json['type'], 'executionResult'); + expect(json['success'], isFalse); + expect(json['exitCode'], 15); + expect(json['message'], contains('Some critical error')); + expect(json['error'], isNotNull); + expect(json['error']['type'], '_Exception'); + expect(json['error']['message'], contains('Some critical error')); + }); + }); + + group('logEnable false', () { + test('build with logEnable false suppresses failure stdout', () async { + const invalidYaml = ''' +property: + workspace: . +commands: + - command: Print + id: hello + param: + message: hi +pipeline: + id: main + workflow: + - exe: doesNotExist +'''; + + final prints = []; + final result = await runZoned(() async { + return await Application.instance.build( + BuildType.file, + yamlContent: invalidYaml, + logEnable: false, + ); + }, zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) { + prints.add(line); + }, + )); + + expect(result.success, isFalse); + expect(prints, isEmpty); + }); + + test('build with logEnable false suppresses command stdout logs', () async { + const yaml = ''' +property: + workspace: . +commands: + - command: Print + id: hello + param: + message: hi +pipeline: + id: main + workflow: + - exe: hello +'''; + + final prints = []; + final result = await runZoned(() async { + return await Application.instance.build( + BuildType.file, + yamlContent: yaml, + logEnable: false, + ); + }, zoneSpecification: ZoneSpecification( + print: (self, parent, zone, line) { + prints.add(line); + }, + )); + + expect(result.success, isTrue); + expect(prints, isEmpty); + }); + }); + + group('CommandExe JSON Mode', () { + test('CommandExe shouldPrintExecuteLog suppresses execute log for json', () { + final command = CommandExe(); + expect(command.shouldPrintExecuteLog(['--json']), isFalse); + expect(command.shouldPrintExecuteLog(['-f', 'file.yaml']), isTrue); + }); + + test('CommandExe exe -f missing file --json returns execution result json', () async { + final missing = '/tmp/oto_missing_${DateTime.now().microsecondsSinceEpoch}.yaml'; + expect(File(missing).existsSync(), isFalse); + + final prints = []; + final command = CommandExe(printString: (value) async { + prints.add(value); + }); + + final priorExitCode = exitCode; + try { + await command.execute(['-f', missing, '--json']); + } finally { + exitCode = priorExitCode; + } + + expect(prints, hasLength(1)); + final json = jsonDecode(prints[0]); + expect(json['schemaVersion'], 1); + expect(json['type'], 'executionResult'); + expect(json['success'], isFalse); + expect(json['exitCode'], 10); + expect(json['message'], contains('There are no files in path')); + }); + + test('actual bin exe --json output has parseable execution result json and no execute logs', () async { + final tempFile = File('${Directory.systemTemp.path}/oto_temp_${DateTime.now().microsecondsSinceEpoch}.yaml'); + const yamlContent = ''' +property: + workspace: . +commands: + - command: Print + id: hello + param: + message: hi +pipeline: + id: main + workflow: + - exe: hello +'''; + tempFile.writeAsStringSync(yamlContent); + + try { + final result = await Process.run('dart', [ + 'run', + 'bin/main.dart', + 'exe', + '-f', + tempFile.path, + '--json', + ]); + + expect(result.exitCode, 0); + + final stdoutStr = result.stdout.toString().trim(); + + expect(stdoutStr, isNotEmpty); + + final json = jsonDecode(stdoutStr); + expect(json['schemaVersion'], 1); + expect(json['type'], 'executionResult'); + expect(json['success'], isTrue); + expect(json['exitCode'], 0); + + expect(stdoutStr, isNot(contains('Execute command:'))); + expect(stdoutStr, isNot(contains('hi'))); + } finally { + if (tempFile.existsSync()) { + tempFile.deleteSync(); + } + } + }); + }); }