diff --git a/agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md b/agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md index ba8ca47..12ada49 100644 --- a/agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md +++ b/agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md @@ -30,6 +30,10 @@ pipeline/core 실행 로직이 CLI 출력 구현에 직접 의존하지 않도 - `apps/runner/lib/oto/pipeline/pipeline_exe.dart`와 `pipeline_foreach.dart`는 CLI 출력 계층을 직접 import한다. - `apps/runner/lib/oto/core/data_composer.dart`는 CLI color/printer 형태를 core 동작으로 노출한다. - 표준선: domain 실행 로직은 출력 port를 호출하고, CLI/UI/server는 port adapter를 제공한다. +- 현재 작업 현황: + - `01_output_port_contract`는 PASS 완료되어 `[port]` Task가 완료 상태다. + - `02+01_pipeline_output_port` active plan/review가 다음 실행 대상이며 `[pipeline]` Task를 완료한다. + - `03+01,02_core_cli_adapter` active plan/review는 `02+01_pipeline_output_port` 완료 후 `[core]`, `[cli-adapter]` Task를 완료한다. ## 기능 diff --git a/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/USER_REVIEW.md b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/USER_REVIEW.md new file mode 100644 index 0000000..401e95c --- /dev/null +++ b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/USER_REVIEW.md @@ -0,0 +1,60 @@ +# User Review Resolved - m-runner-output-boundary/02+01_pipeline_output_port + +## 요청 일시 + +2026-06-07 + +## 상태 + +RESOLVED_PASS + +## 사유 + +- 유형: environment-blocked +- 현재 리뷰 회차: 1 +- 최종 판정: PASS (user-review resolved) +- 요약: pipeline output port 구현과 focused regression은 통과했다. 전체 `cd apps/runner && dart test`의 legacy iop Edge smoke 실패는 현재 iop에 해당 구현이 없어 이 pipeline subtask 완료 범위에서 제외하기로 사용자가 승인했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | 전체 `dart test`가 `test/oto_iop_connection_smoke_test.dart`의 external iop Edge config 실패로 통과하지 못했다. | +| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 사용자가 현재 iop 미구현으로 인한 legacy iop smoke 실패는 이 pipeline subtask 범위에서 제외하는 것이 맞다고 승인했다. | + +## 해소 결정 + +- 결정: legacy iop smoke는 현재 iop에 구현이 없어 이 pipeline output port subtask의 완료 범위에서 제외한다. +- 최종 완료 근거: `cd apps/runner && dart analyze` 통과, `cd apps/runner && dart test test/oto_core_test.dart` 통과, pipeline output이 explicit `ExecutionContext.output`으로 전달되는 regression test 추가 및 통과. +- 미해결 외부 항목: `test/oto_iop_connection_smoke_test.dart`는 별도 agent/iop 범위에서 다룬다. + +## 차단 근거 + +- 문제: `plan_local_G06_0.log`는 최종 검증으로 `cd apps/runner && dart test` 통과를 요구했지만, 전체 runner test가 legacy iop smoke 실패로 종료된다. +- 현재 archive plan: `plan_local_G06_0.log` +- 현재 archive review: `code_review_local_G06_0.log` +- 검증 명령: `cd apps/runner && dart test`; focused 재현 `cd apps/runner && dart test test/oto_iop_connection_smoke_test.dart -p vm --plain-name 'OTO Dart proto-socket client registers with iop Edge and goes online'` +- 실제 출력: 전체 suite는 `00:15 +216 -1: Some tests failed.`로 종료했다. focused 재현은 `iop Edge exited before listening (code 1): Error: load config: nodes[0] alias="oto-smoke": invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패했다. +- 차단 판단 근거: `agent-test/local/agent-smoke.md`는 legacy iop smoke를 독립 OTO Control Plane agent-smoke 필수 검증에서 제외한다고 명시하지만, 이 plan은 pipeline-smoke 최종 검증으로 전체 runner test를 요구했다. 사용자가 legacy iop smoke 실패를 이 pipeline subtask 완료에서 어떻게 취급할지 결정해야 한다. + +## 사용자 결정 필요 + +- [ ] 자동 follow-up plan/review를 계속 진행한다. +- [ ] 계획을 재작성한다. +- [ ] 테스트 환경, secret, 외부 서비스, SDK, 장비 조건을 준비한 뒤 재시도한다. +- [x] 작업 범위를 줄이거나 보류/폐기한다. + +## 재개 조건 + +- 다음 중 하나가 필요하다: 호환되는 iop checkout을 `IOP_REPO_ROOT`로 제공해 전체 `cd apps/runner && dart test`가 통과한다; 별도 agent/iop 범위 작업으로 legacy smoke fixture를 현재 iop 계약에 맞춘다; 또는 사용자가 이 pipeline subtask는 `dart analyze`와 `test/oto_core_test.dart` 통과 및 legacy iop smoke 실패 기록으로 완료 가능하다고 승인한다. + +## 다음 실행 힌트 + +- 재시도 명령: `cd apps/runner && dart test` +- focused 차단 확인: `cd apps/runner && dart test test/oto_iop_connection_smoke_test.dart -p vm --plain-name 'OTO Dart proto-socket client registers with iop Edge and goes online'` +- 작업 경로: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/` + +## 종료 규칙 + +- 사용자가 이 stop state를 완료/PASS로 해소하면 `USER_REVIEW.md`를 해소 상태로 갱신하고, `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. +- 새 구현이 필요하면 `plan` 스킬이 `USER_REVIEW.md`를 `user_review_N.log`로 아카이브한 뒤 새 `PLAN-*-G??.md` / `CODE_REVIEW-*-G??.md`를 작성한다. diff --git a/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/code_review_local_G06_0.log b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/code_review_local_G06_0.log new file mode 100644 index 0000000..5b93ef9 --- /dev/null +++ b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/code_review_local_G06_0.log @@ -0,0 +1,133 @@ + + +# Code Review Reference - REFACTOR + +> **[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. +> Do not ask the user directly; record blockers in `사용자 리뷰 요청`. +> Finalization is review-agent-only. + +## 개요 + +date=2026-06-07 +task=m-runner-output-boundary/02+01_pipeline_output_port, plan=1, tag=REFACTOR + +## Roadmap Targets + +- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md` +- Task ids: + - `pipeline`: pipeline domain이 CLI 구현체 대신 output port를 사용하도록 분리한다. 검증: runner analyze와 pipeline 관련 테스트가 통과한다. +- Completion mode: check-on-pass + +## 이 파일을 읽는 리뷰 에이전트에게 + +> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. + +1. predecessor complete.log 확인 여부를 검증한다. +2. 판정을 append한다. +3. PASS이면 plan/review log화, complete.log 작성, archive 이동을 수행한다. +4. roadmap 수정은 런타임 책임이다. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [REFACTOR-1] Pipeline Output Port Rollout | [x] | + +## 구현 체크리스트 + +- [x] `01_output_port_contract` complete.log를 확인한다. +- [x] `pipeline_exe.dart`, `pipeline_exe_handle.dart`, `pipeline_foreach.dart`에서 CLI import와 CLI direct call을 제거한다. +- [x] pipeline 출력은 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용한다. +- [x] explicit context output regression test를 추가한다. +- [x] `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart`, `cd apps/runner && dart test`를 실행한다. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. + +## 코드리뷰 전용 체크리스트 + +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] active review/plan 파일을 `.log`로 아카이브한다. +- [x] PASS이면 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다. +- [x] PASS split 작업이면 parent 유지/정리 상태를 확인한다. +- [x] PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. +- [x] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되어 `USER_REVIEW.md`를 resolved 상태로 갱신하고 `complete.log`를 작성한다. + +## 계획 대비 변경 사항 + +- `printCondition` 인자를 positional이 아닌 named parameter `{ExecutionContext? context}`로 추가했다. 기존 5개 호출부를 모두 `context: runtimeContext`로 갱신하여 누락 없이 처리했다. +- `pipeline_exe_handle.dart`에서 `Application` import를 제거하려 했으나 `CommandState` enum이 `application.dart`에 정의되어 있어 유지했다. `cli.dart` import만 제거했다. + +## 주요 설계 결정 + +- `printCondition` static 메서드에 `ExecutionContext? context` named optional 파라미터를 추가하고, `(context ?? Application.instance.context).output.line(...)` 패턴으로 fallback 유지. `Application.log`는 로그 파일 경계로 보존했다. +- `buildStep` 호출 시 명시적 `style` 인자 없이 기본값(`RunnerOutputStyle.success`) 사용. `printBuildStep(Color.green)` 의미와 일치한다. +- `PipelineForeach` 완료 출력: `CLI.println(color: Color.magentaStrong)` → `runtimeContext.output.line(style: RunnerOutputStyle.progress)` 매핑. + +## 사용자 리뷰 요청 + +_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ + +- 상태: 없음 +- 사유 유형: 없음 +- 결정 필요: 없음 +- 차단 근거: 없음 +- 실행한 검증/명령: 없음 +- 자동 후속 불가 이유: 없음 +- 재개 조건: 없음 + +## 리뷰어를 위한 체크포인트 + +- pipeline 도메인에서 `package:oto/cli/cli.dart` import가 사라졌는지 확인한다. +- `Application.instance.printBuildStep` 직접 호출이 pipeline에 남지 않았는지 확인한다. +- explicit context output 테스트가 singleton Application output에 기대지 않는지 확인한다. + +## 검증 결과 + +### REFACTOR-1 중간 검증 + +```bash +$ cd apps/runner && dart analyze +Analyzing runner... +No issues found! + +$ cd apps/runner && dart test test/oto_core_test.dart +00:00 +31: All tests passed! +``` + +### 최종 검증 + +```bash +$ cd apps/runner && dart analyze +Analyzing runner... +No issues found! + +$ cd apps/runner && dart test +00:15 +216 -1: Some tests failed. +``` + +> 실패한 1건은 `test/oto_iop_connection_smoke_test.dart: OTO Dart proto-socket client registers with iop Edge and goes online`이다. 재실행한 focused 명령도 `iop Edge exited before listening (code 1): Error: load config: nodes[0] alias="oto-smoke": invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패했다. pipeline 변경과 직접 연결된 `oto_core_test.dart` 31개는 통과했다. + +## 코드리뷰 결과 + +_리뷰 에이전트가 append한다._ + +### 2026-06-07 리뷰 판정 + +- 종합 판정: FAIL +- 차원별 평가: + - Correctness: Pass + - Completeness: Fail + - Test coverage: Pass + - API contract: Pass + - Code quality: Pass + - Plan deviation: Warn + - Verification trust: Fail +- 발견된 문제: + - Required: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log:154`의 최종 검증 계약은 `cd apps/runner && dart test` 통과를 요구하지만, 실제 재현 결과 전체 runner test가 `test/oto_iop_connection_smoke_test.dart` 1건 실패로 종료된다. 실패 근거는 [oto_iop_connection_smoke_test.dart](/config/workspace/oto/apps/runner/test/oto_iop_connection_smoke_test.dart:276)의 legacy iop Edge config가 현재 외부 iop checkout에서 허용하지 않는 `agent_kind: "oto-agent"`를 사용해 `allowed: "generic-node"` 오류가 나는 것이다. 이 pipeline subtask 안에서 임의로 agent/iop smoke 계약을 변경하거나 legacy smoke 제외를 확정할 수 없으므로, 사용자 결정 또는 호환되는 `IOP_REPO_ROOT` 환경이 필요하다. + - Nit: 리뷰 중 `dart format`으로 변경 대상 Dart 파일의 formatting drift를 정리했다. 동작 변경은 없다. +- 다음 단계: + - USER_REVIEW: active plan/review를 로그로 아카이브하고, 전체 `dart test` 실패를 사용자 판단/외부 환경 선결 조건으로 남기는 `USER_REVIEW.md`를 작성한다. diff --git a/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/complete.log b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/complete.log new file mode 100644 index 0000000..40158c1 --- /dev/null +++ b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/complete.log @@ -0,0 +1,44 @@ +# Complete - m-runner-output-boundary/02+01_pipeline_output_port + +## 완료 일시 + +2026-06-07 + +## 요약 + +pipeline domain의 CLI direct output dependency를 explicit `ExecutionContext.output` port로 전환했고, 1회 리뷰에서 legacy iop smoke 범위 판단으로 USER_REVIEW를 거친 뒤 PASS로 종료했다. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_local_G06_0.log` | `code_review_local_G06_0.log` | FAIL | 전체 `dart test`가 legacy iop smoke 1건으로 실패해 사용자 범위 판단이 필요했다. | +| `USER_REVIEW.md` | user decision | PASS/RESOLVED | 현재 iop 미구현으로 인한 legacy iop smoke 실패는 이 pipeline subtask 완료 범위에서 제외한다고 사용자가 승인했다. | + +## 구현/정리 내용 + +- pipeline condition/build-step/foreach progress 출력이 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용하도록 전환했다. +- `apps/runner/lib/oto/pipeline/**`에서 `package:oto/cli/cli.dart` import와 `Application.instance.printBuildStep` direct call을 제거했다. +- explicit context output port regression test를 `apps/runner/test/oto_core_test.dart`에 추가했다. +- 리뷰 중 변경 대상 Dart 파일에 `dart format`을 적용해 formatting drift를 정리했다. + +## 최종 검증 + +- `cd apps/runner && dart analyze` - PASS; `No issues found!` +- `cd apps/runner && dart test test/oto_core_test.dart` - PASS; `+31: All tests passed!` +- `cd apps/runner && dart test` - USER-RESOLVED EXCLUDED; `test/oto_iop_connection_smoke_test.dart`가 `invalid agent_kind "oto-agent" (allowed: "generic-node")`로 실패하지만, 현재 iop 미구현으로 이 pipeline subtask 완료 범위에서 제외한다고 사용자가 승인했다. + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md` +- Completed task ids: + - `pipeline`: PASS; evidence=`agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log`, `agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/code_review_local_G06_0.log`, `agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/USER_REVIEW.md`; verification=`cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- legacy iop smoke는 현재 iop 구현 상태와 맞지 않으므로 별도 agent/iop 범위에서 정리한다. diff --git a/agent-task/m-runner-output-boundary/02+01_pipeline_output_port/PLAN-local-G06.md b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log similarity index 96% rename from agent-task/m-runner-output-boundary/02+01_pipeline_output_port/PLAN-local-G06.md rename to agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log index 53dc24f..d7faaa9 100644 --- a/agent-task/m-runner-output-boundary/02+01_pipeline_output_port/PLAN-local-G06.md +++ b/agent-task/archive/2026/06/m-runner-output-boundary/02+01_pipeline_output_port/plan_local_G06_0.log @@ -15,8 +15,8 @@ ## Roadmap Dependency -- predecessor: `agent-task/m-runner-output-boundary/01_output_port_contract/complete.log` -- 상태: 작성 시점에는 미충족. 구현 시작 전 active 또는 archive complete.log를 확인한다. +- predecessor: `agent-task/archive/2026/06/m-runner-output-boundary/01_output_port_contract/complete.log` +- 상태: 충족. `01_output_port_contract`가 PASS 완료되어 milestone `[port]` Task에 반영되었으므로 이 subtask는 실행 가능하다. ## 분석 결과 diff --git a/agent-task/m-runner-output-boundary/02+01_pipeline_output_port/CODE_REVIEW-local-G06.md b/agent-task/m-runner-output-boundary/02+01_pipeline_output_port/CODE_REVIEW-local-G06.md deleted file mode 100644 index e42c29b..0000000 --- a/agent-task/m-runner-output-boundary/02+01_pipeline_output_port/CODE_REVIEW-local-G06.md +++ /dev/null @@ -1,108 +0,0 @@ - - -# Code Review Reference - REFACTOR - -> **[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. -> Do not ask the user directly; record blockers in `사용자 리뷰 요청`. -> Finalization is review-agent-only. - -## 개요 - -date=2026-06-07 -task=m-runner-output-boundary/02+01_pipeline_output_port, plan=1, tag=REFACTOR - -## Roadmap Targets - -- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/runner-output-boundary.md` -- Task ids: - - `pipeline`: pipeline domain이 CLI 구현체 대신 output port를 사용하도록 분리한다. 검증: runner analyze와 pipeline 관련 테스트가 통과한다. -- Completion mode: check-on-pass - -## 이 파일을 읽는 리뷰 에이전트에게 - -> **[REVIEW AGENT ONLY]** 종결 절차는 코드리뷰 에이전트 전용이다. - -1. predecessor complete.log 확인 여부를 검증한다. -2. 판정을 append한다. -3. PASS이면 plan/review log화, complete.log 작성, archive 이동을 수행한다. -4. roadmap 수정은 런타임 책임이다. - ---- - -## 구현 항목별 완료 여부 - -| 항목 | 완료 여부 | -|------|---------| -| [REFACTOR-1] Pipeline Output Port Rollout | [ ] | - -## 구현 체크리스트 - -- [ ] `01_output_port_contract` complete.log를 확인한다. -- [ ] `pipeline_exe.dart`, `pipeline_exe_handle.dart`, `pipeline_foreach.dart`에서 CLI import와 CLI direct call을 제거한다. -- [ ] pipeline 출력은 `runtimeContext.output.line` 또는 `runtimeContext.output.buildStep`을 사용한다. -- [ ] explicit context output regression test를 추가한다. -- [ ] `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_core_test.dart`, `cd apps/runner && dart test`를 실행한다. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. - -## 코드리뷰 전용 체크리스트 - -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] active review/plan 파일을 `.log`로 아카이브한다. -- [ ] PASS이면 `complete.log` 작성 후 active task 디렉터리를 archive로 이동한다. -- [ ] PASS split 작업이면 parent 유지/정리 상태를 확인한다. -- [ ] PASS이고 task group이 `m-`이면 완료 이벤트 메타데이터를 보고한다. - -## 계획 대비 변경 사항 - -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ - -## 주요 설계 결정 - -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ - -## 사용자 리뷰 요청 - -_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 구현 에이전트는 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._ - -- 상태: 없음 -- 사유 유형: 없음 -- 결정 필요: 없음 -- 차단 근거: 없음 -- 실행한 검증/명령: 없음 -- 자동 후속 불가 이유: 없음 -- 재개 조건: 없음 - -## 리뷰어를 위한 체크포인트 - -- pipeline 도메인에서 `package:oto/cli/cli.dart` import가 사라졌는지 확인한다. -- `Application.instance.printBuildStep` 직접 호출이 pipeline에 남지 않았는지 확인한다. -- explicit context output 테스트가 singleton Application output에 기대지 않는지 확인한다. - -## 검증 결과 - -### REFACTOR-1 중간 검증 - -```bash -$ cd apps/runner && dart analyze -(output) - -$ cd apps/runner && dart test test/oto_core_test.dart -(output) -``` - -### 최종 검증 - -```bash -$ cd apps/runner && dart analyze -(output) - -$ cd apps/runner && dart test -(output) -``` - -## 코드리뷰 결과 - -_리뷰 에이전트가 append한다._ diff --git a/agent-task/m-runner-output-boundary/03+01,02_core_cli_adapter/PLAN-cloud-G07.md b/agent-task/m-runner-output-boundary/03+01,02_core_cli_adapter/PLAN-cloud-G07.md index 202fbc4..7a5ca98 100644 --- a/agent-task/m-runner-output-boundary/03+01,02_core_cli_adapter/PLAN-cloud-G07.md +++ b/agent-task/m-runner-output-boundary/03+01,02_core_cli_adapter/PLAN-cloud-G07.md @@ -16,9 +16,9 @@ ## Roadmap Dependency -- predecessor: `agent-task/m-runner-output-boundary/01_output_port_contract/complete.log` +- predecessor: `agent-task/archive/2026/06/m-runner-output-boundary/01_output_port_contract/complete.log` - predecessor: `agent-task/m-runner-output-boundary/02+01_pipeline_output_port/complete.log` -- 상태: 작성 시점에는 미충족. 구현 시작 전 active 또는 archive complete.log를 확인한다. +- 상태: 부분 충족. `01_output_port_contract`는 PASS 완료되었고, `02+01_pipeline_output_port` 완료 후 이 subtask를 실행한다. ## 분석 결과 diff --git a/apps/runner/lib/oto/pipeline/pipeline_contain.dart b/apps/runner/lib/oto/pipeline/pipeline_contain.dart index b19b490..39ac32c 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_contain.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_contain.dart @@ -60,16 +60,20 @@ class PipelineContain extends PipelineExecutor { } //set true task - var validateResult = - PipelineExecutor.validateTasks(data.on_true, context: context); + var validateResult = PipelineExecutor.validateTasks( + data.on_true, + context: context, + ); if (!validateResult.enable) { return validateResult; } _pipelineTrue = validateResult.pipeline; //set false task - validateResult = - PipelineExecutor.validateTasks(data.on_false, context: context); + validateResult = PipelineExecutor.validateTasks( + data.on_false, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -82,21 +86,30 @@ class PipelineContain extends PipelineExecutor { @override Future execute() async { var result = ConditionCheckResult(); - var value = TagSystem.replaceSingleTagValue(data.target, context: runtimeContext); + var value = TagSystem.replaceSingleTagValue( + data.target, + context: runtimeContext, + ); value ??= data.target; result.conditionValue = 'Contain in "$value".'; var listRaw = data.list; - if(listRaw is String) { - var str = TagSystem.replaceTagValue(listRaw, context: runtimeContext) as String; + if (listRaw is String) { + var str = + TagSystem.replaceTagValue(listRaw, context: runtimeContext) as String; result.result = str.contains(value); } else { var list = listRaw as List; result.result = list.contains(value); } - - - await PipelineExecutor.printCondition('Contain', 'Contain in "${data.target}"', getPostfix(result.result), result); + + await PipelineExecutor.printCondition( + 'Contain', + 'Contain in "${data.target}"', + getPostfix(result.result), + result, + context: runtimeContext, + ); if (result.result) { await _pipelineTrue?.execute(); } else { diff --git a/apps/runner/lib/oto/pipeline/pipeline_exe.dart b/apps/runner/lib/oto/pipeline/pipeline_exe.dart index 858aa15..fd1c5ea 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_exe.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_exe.dart @@ -1,8 +1,8 @@ import 'package:dart_framework/utils/system_util.dart'; -import 'package:oto/cli/cli.dart'; import 'package:oto/oto/application.dart'; import 'package:oto/oto/commands/command.dart'; import 'package:oto/oto/core/execution_context.dart'; +import 'package:oto/oto/core/output_port.dart'; import 'package:oto/oto/data/command_data.dart'; import 'package:oto/oto/pipeline/pipeline.dart'; import 'package:oto/oto/pipeline/pipeline_condition.dart'; @@ -29,8 +29,10 @@ abstract class PipelineExecutor { PipelineValidateResult initialize(Map set); Future execute(); - static PipelineValidateResult validateTasks(List? list, - {ExecutionContext? context}) { + static PipelineValidateResult validateTasks( + List? list, { + ExecutionContext? context, + }) { var result = PipelineValidateResult(); if (list != null) { return Pipeline.pipelineInitialize(list, context: context); @@ -38,11 +40,20 @@ abstract class PipelineExecutor { return result; } - static Future printCondition(String syntax, String condition, String postfix, - ConditionCheckResult result) async { - await CLI.println('[Pipeline-$syntax]', color: Color.magentaStrong); + static Future printCondition( + String syntax, + String condition, + String postfix, + ConditionCheckResult result, { + ExecutionContext? context, + }) async { + await (context ?? Application.instance.context).output.line( + '[Pipeline-$syntax]', + style: RunnerOutputStyle.progress, + ); Application.log( - ' - Condition: $condition\n - Values: ${result.conditionValue}\n - Result: ${result.result} ===> $postfix'); + ' - Condition: $condition\n - Values: ${result.conditionValue}\n - Result: ${result.result} ===> $postfix', + ); return simpleFuture; } @@ -90,9 +101,9 @@ class PipelineExe extends PipelineExecutor { Future get commandData async { var commandData = runtimeContext.dataCommandMap[commandID]!; - await Application.instance.printBuildStep( - '${printPrefix}Phase Start: ${commandData.name} ($commandID)', - Color.green); + await runtimeContext.output.buildStep( + '${printPrefix}Phase Start: ${commandData.name} ($commandID)', + ); return dataFutrue(commandData); } diff --git a/apps/runner/lib/oto/pipeline/pipeline_exe_handle.dart b/apps/runner/lib/oto/pipeline/pipeline_exe_handle.dart index fb1e641..48d450c 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_exe_handle.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_exe_handle.dart @@ -1,5 +1,4 @@ import 'package:dart_framework/utils/system_util.dart'; -import 'package:oto/cli/cli.dart'; import 'package:oto/oto/application.dart'; import 'package:oto/oto/commands/command.dart'; import 'package:oto/oto/data/command_data.dart'; @@ -89,16 +88,20 @@ class PipelineExeHandle extends PipelineExecutor { handleData = ExeHandleData(); //set success task - var validateResult = - PipelineExecutor.validateTasks(data.on_success, context: context); + var validateResult = PipelineExecutor.validateTasks( + data.on_success, + context: context, + ); if (!validateResult.enable) { return validateResult; } handleData.pipelineSuccess = validateResult.pipeline; //set fail task - validateResult = - PipelineExecutor.validateTasks(data.on_fail, context: context); + validateResult = PipelineExecutor.validateTasks( + data.on_fail, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -111,8 +114,9 @@ class PipelineExeHandle extends PipelineExecutor { Future get commandData async { var commandID = data.id; var commandData = runtimeContext.dataCommandMap[commandID]!; - await Application.instance.printBuildStep( - 'Phase Start: ${commandData.name} ($commandID)', Color.green); + await runtimeContext.output.buildStep( + 'Phase Start: ${commandData.name} ($commandID)', + ); return dataFutrue(commandData); } diff --git a/apps/runner/lib/oto/pipeline/pipeline_foreach.dart b/apps/runner/lib/oto/pipeline/pipeline_foreach.dart index 83c27ae..a737ce4 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_foreach.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_foreach.dart @@ -1,6 +1,6 @@ import 'package:dart_framework/utils/system_util.dart'; -import 'package:oto/cli/cli.dart'; import 'package:oto/oto/application.dart'; +import 'package:oto/oto/core/output_port.dart'; import 'package:oto/oto/core/tag_system.dart'; import 'package:oto/oto/data/pipeline_data.dart'; import 'package:oto/oto/pipeline/pipeline.dart'; @@ -71,8 +71,10 @@ class PipelineForeach extends PipelineExecutor { } //set do task - var validateResult = - PipelineExecutor.validateTasks(data.on_do, context: context); + var validateResult = PipelineExecutor.validateTasks( + data.on_do, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -83,37 +85,49 @@ class PipelineForeach extends PipelineExecutor { @override Future execute() async { - var iterator = TagSystem.replaceTagValue(data.iterator!, context: runtimeContext); + var iterator = TagSystem.replaceTagValue( + data.iterator!, + context: runtimeContext, + ); var iteratorType = ''; - if(iterator != null) - { - if(iterator is List) - { + if (iterator != null) { + if (iterator is List) { iteratorType = 'List'; var elementTag = data.setValue; - for(var item in iterator) - { - await TagSystem.setPropertyValue(elementTag, item, context: runtimeContext); + for (var item in iterator) { + await TagSystem.setPropertyValue( + elementTag, + item, + context: runtimeContext, + ); await _pipelineOnDo?.execute(); } } - if(iterator is Map) - { + if (iterator is Map) { iteratorType = 'Map'; var keyTag = data.setKey; var elementTag = data.setValue; - for(var pair in iterator.entries) - { - if(keyTag != null) - { - await TagSystem.setPropertyValue(keyTag, pair.key, context: runtimeContext); + for (var pair in iterator.entries) { + if (keyTag != null) { + await TagSystem.setPropertyValue( + keyTag, + pair.key, + context: runtimeContext, + ); } - await TagSystem.setPropertyValue(elementTag, pair.value, context: runtimeContext); + await TagSystem.setPropertyValue( + elementTag, + pair.value, + context: runtimeContext, + ); await _pipelineOnDo?.execute(); } } - await CLI.println('[Pipeline-Foreach ($iteratorType)]', color: Color.magentaStrong); + await runtimeContext.output.line( + '[Pipeline-Foreach ($iteratorType)]', + style: RunnerOutputStyle.progress, + ); Application.log(' - Foreach complete. Loop out'); } return simpleFuture; diff --git a/apps/runner/lib/oto/pipeline/pipeline_if.dart b/apps/runner/lib/oto/pipeline/pipeline_if.dart index 1236d15..0b47a55 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_if.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_if.dart @@ -64,16 +64,20 @@ class PipelineIf extends PipelineCondition { } //set true task - var validateResult = - PipelineExecutor.validateTasks(data.on_true, context: context); + var validateResult = PipelineExecutor.validateTasks( + data.on_true, + context: context, + ); if (!validateResult.enable) { return validateResult; } _pipelineTrue = validateResult.pipeline; //set false task - validateResult = - PipelineExecutor.validateTasks(data.on_false, context: context); + validateResult = PipelineExecutor.validateTasks( + data.on_false, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -87,7 +91,12 @@ class PipelineIf extends PipelineCondition { var condition = data.condition!; var result = checkCondition(condition); await PipelineExecutor.printCondition( - 'IF', condition, getPostfix(result.result), result); + 'IF', + condition, + getPostfix(result.result), + result, + context: runtimeContext, + ); if (result.result) { await _pipelineTrue?.execute(); } else { diff --git a/apps/runner/lib/oto/pipeline/pipeline_switch.dart b/apps/runner/lib/oto/pipeline/pipeline_switch.dart index c9a1cfd..911ba79 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_switch.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_switch.dart @@ -70,7 +70,10 @@ class PipelineSwitch extends PipelineCondition { } for (var item in data.cases) { - var validateResult = PipelineExecutor.validateTasks(item.tasks, context: context); + var validateResult = PipelineExecutor.validateTasks( + item.tasks, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -90,10 +93,18 @@ class PipelineSwitch extends PipelineCondition { var result = checkCondition(addCondition); if (result.result) { executed = true; - var validateResult = - PipelineExecutor.validateTasks(item.tasks, context: context); + var validateResult = PipelineExecutor.validateTasks( + item.tasks, + context: context, + ); _pipeline = validateResult.pipeline; - await PipelineExecutor.printCondition('Switch', data.condition!, 'Start task', result); + await PipelineExecutor.printCondition( + 'Switch', + data.condition!, + 'Start task', + result, + context: runtimeContext, + ); await _pipeline?.execute(); break; } @@ -103,7 +114,12 @@ class PipelineSwitch extends PipelineCondition { result.conditionValue = 'null'; result.result = false; await PipelineExecutor.printCondition( - 'Switch', data.condition!, 'not exist Same condition', result); + 'Switch', + data.condition!, + 'not exist Same condition', + result, + context: runtimeContext, + ); } return simpleFuture; } diff --git a/apps/runner/lib/oto/pipeline/pipeline_wait_until.dart b/apps/runner/lib/oto/pipeline/pipeline_wait_until.dart index b42cd33..f63d84d 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_wait_until.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_wait_until.dart @@ -41,13 +41,23 @@ class PipelineWaitUntil extends PipelineCondition { var condition = data.condition!; var result = checkCondition(condition); await PipelineExecutor.printCondition( - 'Wait-Until', condition, 'Start wait.', result); + 'Wait-Until', + condition, + 'Start wait.', + result, + context: runtimeContext, + ); while (result.result) { await Future.delayed(const Duration(milliseconds: 200)); result = checkCondition(data.condition!); } await PipelineExecutor.printCondition( - 'Wait-Until', condition, 'Loop escape.', result); + 'Wait-Until', + condition, + 'Loop escape.', + result, + context: runtimeContext, + ); return simpleFuture; } } diff --git a/apps/runner/lib/oto/pipeline/pipeline_while.dart b/apps/runner/lib/oto/pipeline/pipeline_while.dart index d188a36..fd9ec31 100644 --- a/apps/runner/lib/oto/pipeline/pipeline_while.dart +++ b/apps/runner/lib/oto/pipeline/pipeline_while.dart @@ -52,8 +52,10 @@ class PipelineWhile extends PipelineCondition { } //set do task - var validateResult = - PipelineExecutor.validateTasks(data.on_do, context: context); + var validateResult = PipelineExecutor.validateTasks( + data.on_do, + context: context, + ); if (!validateResult.enable) { return validateResult; } @@ -68,12 +70,22 @@ class PipelineWhile extends PipelineCondition { var result = checkCondition(condition); while (result.result) { await PipelineExecutor.printCondition( - 'While', condition, 'Execute: on-do', result); + 'While', + condition, + 'Execute: on-do', + result, + context: runtimeContext, + ); await _pipelineOnDo?.execute(); result = checkCondition(data.condition!); } await PipelineExecutor.printCondition( - 'While', condition, 'Loop escaped.', result); + 'While', + condition, + 'Loop escaped.', + result, + context: runtimeContext, + ); return simpleFuture; } } diff --git a/apps/runner/test/oto_core_test.dart b/apps/runner/test/oto_core_test.dart index 4ef8d31..1abc321 100644 --- a/apps/runner/test/oto_core_test.dart +++ b/apps/runner/test/oto_core_test.dart @@ -59,7 +59,7 @@ pipeline: test('pipeline validation rejects missing command id', () { final result = Pipeline.pipelineInitialize([ - {'exe': 'doesNotExist'} + {'exe': 'doesNotExist'}, ]); expect(result.enable, isFalse); expect(result.message, contains('doesNotExist')); @@ -70,7 +70,9 @@ pipeline: // and asserts that flow-control uses the explicit context exclusively. ExecutionContext makeCtx( - Map props, List commandIds) { + Map props, + List commandIds, + ) { registerAllCommands(); final ctx = ExecutionContext(); ctx.property.addAll(props); @@ -84,98 +86,105 @@ pipeline: return ctx; } - test('PipelineIf evaluates condition from explicit context not singleton', - () async { - // Singleton: env = 'dev' → on-false; explicit: env = 'prod' → on-true - Application.instance.property['env'] = 'dev'; - final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']); + test( + 'PipelineIf evaluates condition from explicit context not singleton', + () async { + // Singleton: env = 'dev' → on-false; explicit: env = 'prod' → on-true + Application.instance.property['env'] = 'dev'; + final ctx = makeCtx({'env': 'prod'}, ['onTrue', 'onFalse']); - final result = Pipeline.pipelineInitialize([ - { - 'if': { - 'condition-string': ' == prod', - 'on-true': [ - {'exe': 'onTrue'} - ], - 'on-false': [ - {'exe': 'onFalse'} - ], - } - } - ], context: ctx); + final result = Pipeline.pipelineInitialize([ + { + 'if': { + 'condition-string': ' == prod', + 'on-true': [ + {'exe': 'onTrue'}, + ], + 'on-false': [ + {'exe': 'onFalse'}, + ], + }, + }, + ], context: ctx); - expect(result.enable, isTrue, reason: result.message); - await result.pipeline!.execute(); + expect(result.enable, isTrue, reason: result.message); + await result.pipeline!.execute(); - expect(ctx.commandStates['onTrue'], CommandState.complete); - expect(ctx.commandStates['onFalse'], CommandState.ready); - }); + expect(ctx.commandStates['onTrue'], CommandState.complete); + expect(ctx.commandStates['onFalse'], CommandState.ready); + }, + ); - test('PipelineForeach iterates list from explicit context not singleton', - () async { - // Singleton: items = [] (empty); explicit: items = [10, 20, 30] - Application.instance.property['items'] = []; - final ctx = makeCtx({ - 'items': [10, 20, 30] - }, [ - 'print' - ]); + test( + 'PipelineForeach iterates list from explicit context not singleton', + () async { + // Singleton: items = [] (empty); explicit: items = [10, 20, 30] + Application.instance.property['items'] = []; + final ctx = makeCtx( + { + 'items': [10, 20, 30], + }, + ['print'], + ); - final result = Pipeline.pipelineInitialize([ - { - 'foreach': { - 'iterator': '', - 'setValue': '<@property.current>', - 'on-do': [ - {'exe': 'print'} - ], - } - } - ], context: ctx); + final result = Pipeline.pipelineInitialize([ + { + 'foreach': { + 'iterator': '', + 'setValue': '<@property.current>', + 'on-do': [ + {'exe': 'print'}, + ], + }, + }, + ], context: ctx); - expect(result.enable, isTrue, reason: result.message); - await result.pipeline!.execute(); + expect(result.enable, isTrue, reason: result.message); + await result.pipeline!.execute(); - // Loop ran 3 times using explicit context; last assigned value = 30 - expect(ctx.property['current'], 30); - // Singleton must not have been modified - expect(Application.instance.property.containsKey('current'), isFalse); - }); + // Loop ran 3 times using explicit context; last assigned value = 30 + expect(ctx.property['current'], 30); + // Singleton must not have been modified + expect(Application.instance.property.containsKey('current'), isFalse); + }, + ); - test('PipelineContain evaluates target from explicit context not singleton', - () async { - // Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list) - Application.instance.property['target'] = 'xyz'; - final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']); + test( + 'PipelineContain evaluates target from explicit context not singleton', + () async { + // Singleton: target = 'xyz' (not in list); explicit: target = 'foo' (in list) + Application.instance.property['target'] = 'xyz'; + final ctx = makeCtx({'target': 'foo'}, ['onTrue', 'onFalse']); - final result = Pipeline.pipelineInitialize([ - { - 'contain': { - 'target': '', - 'list': ['foo', 'bar'], - 'on-true': [ - {'exe': 'onTrue'} - ], - 'on-false': [ - {'exe': 'onFalse'} - ], - } - } - ], context: ctx); + final result = Pipeline.pipelineInitialize([ + { + 'contain': { + 'target': '', + 'list': ['foo', 'bar'], + 'on-true': [ + {'exe': 'onTrue'}, + ], + 'on-false': [ + {'exe': 'onFalse'}, + ], + }, + }, + ], context: ctx); - expect(result.enable, isTrue, reason: result.message); - await result.pipeline!.execute(); + expect(result.enable, isTrue, reason: result.message); + await result.pipeline!.execute(); - expect(ctx.commandStates['onTrue'], CommandState.complete); - expect(ctx.commandStates['onFalse'], CommandState.ready); - }); + expect(ctx.commandStates['onTrue'], CommandState.complete); + expect(ctx.commandStates['onFalse'], CommandState.ready); + }, + ); // REVIEW_VALIDATE-2: malformed nested pipeline regression tests test('pipeline validation rejects if with no condition field', () { final result = Pipeline.pipelineInitialize([ { - 'if': {'on-true': []} - } + 'if': {'on-true': []}, + }, ]); expect(result.enable, isFalse); expect(result.message, isNotNull); @@ -189,10 +198,10 @@ pipeline: 'iterator': 123, 'setValue': '<@property.item>', 'on-do': [ - {'exe': 'someCmd'} + {'exe': 'someCmd'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNotNull); @@ -203,11 +212,8 @@ pipeline: test('pipeline validation rejects switch with non-list cases', () { final result = Pipeline.pipelineInitialize([ { - 'switch': { - 'condition-string': 'test', - 'cases': 'not-a-list', - } - } + 'switch': {'condition-string': 'test', 'cases': 'not-a-list'}, + }, ]); expect(result.enable, isFalse); expect(result.message, isNotNull); @@ -221,10 +227,10 @@ pipeline: 'if': { 'condition-string': '', 'on-true': [ - {'exe': 'hello'} + {'exe': 'hello'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, contains('supported operator')); @@ -237,10 +243,10 @@ pipeline: 'while': { 'condition-bool': '', 'on-do': [ - {'exe': 'hello'} + {'exe': 'hello'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, contains('supported operator')); @@ -249,7 +255,7 @@ pipeline: test('pipeline validation rejects wait-until condition without operator', () { final result = Pipeline.pipelineInitialize([ - {'wait-until-string': ''} + {'wait-until-string': ''}, ]); expect(result.enable, isFalse); expect(result.message, contains('supported operator')); @@ -264,10 +270,10 @@ pipeline: 'id': 'hello', 'on-success': 'not-a-list', 'on-fail': [ - {'exe': 'hello'} + {'exe': 'hello'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -281,11 +287,11 @@ pipeline: 'exe-handle': { 'id': 'hello', 'on-success': [ - {'exe': 'hello'} + {'exe': 'hello'}, ], 'on-fail': 'not-a-list', - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -296,11 +302,8 @@ pipeline: test('pipeline validation rejects if with non-list on-true', () { final result = Pipeline.pipelineInitialize([ { - 'if': { - 'condition-string': 'a == b', - 'on-true': 'not-a-list', - } - } + 'if': {'condition-string': 'a == b', 'on-true': 'not-a-list'}, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -314,11 +317,11 @@ pipeline: 'if': { 'condition-string': 'a == b', 'on-true': [ - {'exe': 'hello'} + {'exe': 'hello'}, ], 'on-false': 'not-a-list', - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -329,11 +332,8 @@ pipeline: test('pipeline validation rejects while with non-list on-do', () { final result = Pipeline.pipelineInitialize([ { - 'while': { - 'condition-bool': 'true', - 'on-do': 'not-a-list', - } - } + 'while': {'condition-bool': 'true', 'on-do': 'not-a-list'}, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -348,8 +348,8 @@ pipeline: 'iterator': '', 'setValue': '<@property.item>', 'on-do': 'not-a-list', - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -363,8 +363,8 @@ pipeline: 'switch': { 'condition-string': 'prod', 'cases': ['not-a-map'], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -378,10 +378,10 @@ pipeline: 'switch': { 'condition-string': 'prod', 'cases': [ - {'value': 'prod'} + {'value': 'prod'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -395,10 +395,10 @@ pipeline: 'switch': { 'condition-string': 'prod', 'cases': [ - {'value': 'prod', 'tasks': 'not-a-list'} + {'value': 'prod', 'tasks': 'not-a-list'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -409,7 +409,7 @@ pipeline: // REVIEW_REVIEW_VALIDATE-2: contain node guard regression tests test('pipeline validation rejects contain with non-map value', () { final result = Pipeline.pipelineInitialize([ - {'contain': 123} + {'contain': 123}, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -422,10 +422,10 @@ pipeline: 'contain': { 'list': ['a', 'b'], 'on-true': [ - {'exe': 'x'} + {'exe': 'x'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -440,10 +440,10 @@ pipeline: 'target': 123, 'list': ['a', 'b'], 'on-true': [ - {'exe': 'x'} + {'exe': 'x'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -457,10 +457,10 @@ pipeline: 'contain': { 'target': 'foo', 'on-true': [ - {'exe': 'x'} + {'exe': 'x'}, ], - } - } + }, + }, ]); expect(result.enable, isFalse); expect(result.message, isNot(contains('is not a subtype'))); @@ -469,67 +469,70 @@ pipeline: }); test( - 'PipelineSwitch validate uses explicit context command map not singleton', - () { - // Singleton dataCommandMap is empty; only explicit context has the command. - registerAllCommands(); - final ctx = ExecutionContext(); - final cmd = DataCommand() - ..command = CommandType.Print - ..id = 'deployProd' - ..param = {'message': 'prod'}; - ctx.dataCommandMap['deployProd'] = cmd; + 'PipelineSwitch validate uses explicit context command map not singleton', + () { + // Singleton dataCommandMap is empty; only explicit context has the command. + registerAllCommands(); + final ctx = ExecutionContext(); + final cmd = DataCommand() + ..command = CommandType.Print + ..id = 'deployProd' + ..param = {'message': 'prod'}; + ctx.dataCommandMap['deployProd'] = cmd; - final result = Pipeline.pipelineInitialize([ - { - 'switch': { - 'condition-string': 'prod', - 'cases': [ - { - 'value': 'prod', - 'tasks': [ - {'exe': 'deployProd'} - ], - } - ], - } - } - ], context: ctx); + final result = Pipeline.pipelineInitialize([ + { + 'switch': { + 'condition-string': 'prod', + 'cases': [ + { + 'value': 'prod', + 'tasks': [ + {'exe': 'deployProd'}, + ], + }, + ], + }, + }, + ], context: ctx); - // Should succeed: deployProd exists in explicit context. - // Would fail if singleton (empty) context were used. - expect(result.enable, isTrue, reason: result.message); - }); + // Should succeed: deployProd exists in explicit context. + // Would fail if singleton (empty) context were used. + expect(result.enable, isTrue, reason: result.message); + }, + ); - test('pipeline execution records started and completed step events', - () async { - final ctx = makeCtx({}, ['hello']); - final result = Pipeline.pipelineInitialize([ - {'exe': 'hello'} - ], context: ctx); + test( + 'pipeline execution records started and completed step events', + () async { + final ctx = makeCtx({}, ['hello']); + final result = Pipeline.pipelineInitialize([ + {'exe': 'hello'}, + ], context: ctx); - expect(result.enable, isTrue); - final pipeline = result.pipeline!; - await pipeline.execute(); + expect(result.enable, isTrue); + final pipeline = result.pipeline!; + await pipeline.execute(); - expect(ctx.stepEvents, hasLength(2)); - final started = ctx.stepEvents[0]; - final completed = ctx.stepEvents[1]; + expect(ctx.stepEvents, hasLength(2)); + final started = ctx.stepEvents[0]; + final completed = ctx.stepEvents[1]; - expect(started.event, 'started'); - expect(started.stepId, 0); - expect(started.workflowIndex, 0); - expect(started.stepType, 'exe'); - expect(started.commandId, 'hello'); - expect(started.commandType, 'Print'); + expect(started.event, 'started'); + expect(started.stepId, 0); + expect(started.workflowIndex, 0); + expect(started.stepType, 'exe'); + expect(started.commandId, 'hello'); + expect(started.commandType, 'Print'); - expect(completed.event, 'completed'); - expect(completed.stepId, 0); - expect(completed.workflowIndex, 0); - expect(completed.stepType, 'exe'); - expect(completed.commandId, 'hello'); - expect(completed.commandType, 'Print'); - }); + expect(completed.event, 'completed'); + expect(completed.stepId, 0); + expect(completed.workflowIndex, 0); + expect(completed.stepType, 'exe'); + expect(completed.commandId, 'hello'); + expect(completed.commandType, 'Print'); + }, + ); test('pipeline execution records failed step event before rethrow', () async { final ctx = ExecutionContext(); @@ -541,8 +544,13 @@ pipeline: await expectLater( pipeline.execute(), - throwsA(isA().having( - (e) => e.toString(), 'message', contains('Test error message'))), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('Test error message'), + ), + ), ); expect(ctx.stepEvents, hasLength(2)); @@ -564,16 +572,70 @@ pipeline: expect(ctx.output, isA()); await expectLater(ctx.output.line('hello'), completes); await expectLater( - ctx.output.block('block', style: RunnerOutputStyle.success), completes); + ctx.output.block('block', style: RunnerOutputStyle.success), + completes, + ); await expectLater( - ctx.output.buildStep('step', style: RunnerOutputStyle.accent), - completes); + ctx.output.buildStep('step', style: RunnerOutputStyle.accent), + completes, + ); final recording = _RecordingOutputPort(); ctx.output = recording; await ctx.output.line('msg', style: RunnerOutputStyle.warning); expect(recording.lines, contains('msg')); }); + + test( + 'pipeline emits progress through explicit context output port', + () async { + final ctx = makeCtx({}, ['hello']); + final recording = _RecordingOutputPort(); + ctx.output = recording; + + final result = Pipeline.pipelineInitialize([ + {'exe': 'hello'}, + ], context: ctx); + + expect(result.enable, isTrue, reason: result.message); + await result.pipeline!.execute(); + + expect(recording.steps, anyElement(contains('hello'))); + expect(Application.instance.context.output, isA()); + }, + ); + + test( + 'PipelineForeach emits completion through explicit context output port', + () async { + final ctx = makeCtx( + { + 'items': [1, 2], + }, + ['print'], + ); + final recording = _RecordingOutputPort(); + ctx.output = recording; + + final result = Pipeline.pipelineInitialize([ + { + 'foreach': { + 'iterator': '', + 'setValue': '<@property.current>', + 'on-do': [ + {'exe': 'print'}, + ], + }, + }, + ], context: ctx); + + expect(result.enable, isTrue, reason: result.message); + await result.pipeline!.execute(); + + expect(recording.lines, anyElement(contains('Foreach'))); + expect(Application.instance.context.output, isA()); + }, + ); } class ThrowingExecutor extends PipelineExecutor { @@ -591,20 +653,26 @@ class _RecordingOutputPort implements RunnerOutputPort { final List steps = []; @override - Future line(String message, - {RunnerOutputStyle style = RunnerOutputStyle.normal}) async { + Future line( + String message, { + RunnerOutputStyle style = RunnerOutputStyle.normal, + }) async { lines.add(message); } @override - Future block(String message, - {RunnerOutputStyle style = RunnerOutputStyle.normal}) async { + Future block( + String message, { + RunnerOutputStyle style = RunnerOutputStyle.normal, + }) async { blocks.add(message); } @override - Future buildStep(String name, - {RunnerOutputStyle style = RunnerOutputStyle.success}) async { + Future buildStep( + String name, { + RunnerOutputStyle style = RunnerOutputStyle.success, + }) async { steps.add(name); } }