From ffc30cb92797b44b66d8e0005b208c43e541fd84 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 16 May 2026 22:59:38 +0900 Subject: [PATCH] feat(edge): add console foreground dedupe task and update console events - Add edge_console_foreground_dedupe agent-task with plan and code review logs - Update console_events.go with foreground dedupe support - Update console_test.go with related test cases - Update e2e-smoke SKILL.md --- agent-ops/skills/project/e2e-smoke/SKILL.md | 1 + .../code_review_cloud_G07_0.log | 183 +++++++++++++++++ .../complete.log | 20 ++ .../plan_cloud_G07_0.log | 192 ++++++++++++++++++ apps/edge/cmd/edge/console_events.go | 4 + apps/edge/cmd/edge/console_test.go | 21 +- 6 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 agent-task/edge_console_foreground_dedupe/code_review_cloud_G07_0.log create mode 100644 agent-task/edge_console_foreground_dedupe/complete.log create mode 100644 agent-task/edge_console_foreground_dedupe/plan_cloud_G07_0.log diff --git a/agent-ops/skills/project/e2e-smoke/SKILL.md b/agent-ops/skills/project/e2e-smoke/SKILL.md index 083272e..c07a6e6 100644 --- a/agent-ops/skills/project/e2e-smoke/SKILL.md +++ b/agent-ops/skills/project/e2e-smoke/SKILL.md @@ -53,6 +53,7 @@ description: bin/edge.sh와 bin/node.sh 기반 사용자 실행 파이프라인 - background off 기본값에서 foreground 응답 대기가 complete event까지 이어지는지 확인한다. - `/session ` 변경 후 메시지를 보내고 출력의 session 값과 node event/message 흐름이 유지되는지 확인한다. - `/terminate-session` 명령이 성공 출력 또는 mock adapter의 명확한 unsupported/error 출력을 내는지 확인한다. + - foreground mock smoke에서는 `` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다. 4. **edge console / transport 항목 확인** - node 연결/해제 lifecycle event가 run event와 분리되어 출력되는지 확인한다. diff --git a/agent-task/edge_console_foreground_dedupe/code_review_cloud_G07_0.log b/agent-task/edge_console_foreground_dedupe/code_review_cloud_G07_0.log new file mode 100644 index 0000000..a68e9d4 --- /dev/null +++ b/agent-task/edge_console_foreground_dedupe/code_review_cloud_G07_0.log @@ -0,0 +1,183 @@ + + +# Code Review Reference - ECF + +> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.** +> The task is NOT complete until every section below is filled in. +> Follow the ownership table at the bottom of this file for which sections you own. + +## 개요 + +date=2026-05-16 +task=edge_console_foreground_dedupe, plan=0, tag=ECF + +## 이 파일을 읽는 리뷰 에이전트에게 + +각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요. +리뷰 완료 후 반드시 아래 순서로 아카이브하세요. + +1. `CODE_REVIEW-cloud-G07.md` → `code_review_cloud_G07_N.log` (N = 기존 code_review_*.log 수) +2. `PLAN-cloud-G07.md` → `plan_cloud_G07_M.log` (M = 기존 plan_*.log 수) +3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성. + +--- + +## 구현 항목별 완료 여부 + +| 항목 | 완료 여부 | +|------|---------| +| [ECF-1] Foreground Run Async 출력 차단 | [x] | +| [ECF-2] e2e-smoke 기준에 중복 출력 실패 조건 반영 | [x] | + +## 계획 대비 변경 사항 + +- `apps/edge/cmd/edge/console_test.go`의 기존 `TestConsoleEventRouterPrintsUnregisteredRun`에서 delta/complete event 인스턴스에 `Background: true`를 추가했다. 계획에서는 "유지"로 명시했으나, ECF-1의 변경(`!event.GetBackground()` early return)으로 인해 background 플래그가 누락된 event는 더 이상 출력되지 않으므로 테스트 데이터 보정이 필요했다. 실제 node는 `RunEvent.Background`를 모든 event에 채워 보내므로(계획 분석 결과 항목 참조) 보정된 데이터가 실 운영과 일치한다. + +## 주요 설계 결정 + +- `consoleEventRouter.Handle`의 책임을 background/async event 출력으로 명확히 한다. `RunEvent.Background == false`인 event는 foreground 경로(`sendConsoleRun`)가 동기 출력하므로 async 경로에서는 tracking 상태와 무관하게 무시한다. +- 기존 `foreground` map 기반 suppress 로직과 lifecycle은 그대로 유지하여 방어선을 이중으로 둔다. background event는 변경된 경로에 영향을 받지 않으므로 회귀 테스트(`TestConsoleEventRouterPrintsUnregisteredRun` 등)는 계속 동일한 출력을 검증한다. +- nil event도 early return 처리해 라우터 호출부에서 추가 nil 검사를 강요하지 않는다. + +## 리뷰어를 위한 체크포인트 + +- foreground run event가 tracking 해제 후 async `Handle`로 들어와도 출력되지 않는지 확인한다. +- background run async 출력은 기존처럼 유지되는지 확인한다. +- `` line이 foreground 중복 complete의 부작용으로 다시 생기지 않는지 확인한다. +- `e2e-smoke` skill에 이번 회귀를 잡는 체크 조건이 추가되었는지 확인한다. +- 검증 결과에 실제 stdout/stderr가 기록되었는지 확인한다. + +## 검증 결과 + +_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._ + +필수 규칙: +- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다. +- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다. +- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다. + +### ECF-1 중간 검증 +``` +$ go test -count=1 ./apps/edge/cmd/edge +ok iop/apps/edge/cmd/edge 0.004s +``` + +### ECF-2 중간 검증 +``` +$ rg --sort path -n "|complete 출력 수|foreground mock smoke" agent-ops/skills/project/e2e-smoke/SKILL.md +56: - foreground mock smoke에서는 `` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다. +``` + +### 최종 검증 +``` +$ go test -count=1 ./apps/edge/cmd/edge +ok iop/apps/edge/cmd/edge 0.004s +``` + +``` +$ go test -count=1 ./... +? iop/apps/control-plane/cmd/control-plane [no test files] +ok iop/apps/edge/cmd/edge 0.009s +? iop/apps/edge/internal/bootstrap [no test files] +ok iop/apps/edge/internal/events 0.007s +ok iop/apps/edge/internal/node 0.006s +ok iop/apps/edge/internal/service 0.009s +ok iop/apps/edge/internal/transport 0.014s +? iop/apps/node/cmd/node [no test files] +ok iop/apps/node/internal/adapters 0.011s +ok iop/apps/node/internal/adapters/cli 87.812s +? iop/apps/node/internal/adapters/cli/internal/testutil [no test files] +ok iop/apps/node/internal/adapters/cli/status 31.118s +? iop/apps/node/internal/adapters/mock [no test files] +? iop/apps/node/internal/adapters/ollama [no test files] +? iop/apps/node/internal/adapters/vllm [no test files] +ok iop/apps/node/internal/bootstrap 0.163s +ok iop/apps/node/internal/node 0.008s +ok iop/apps/node/internal/router 0.006s +? iop/apps/node/internal/runtime [no test files] +ok iop/apps/node/internal/store 0.031s +ok iop/apps/node/internal/transport 0.006s +? iop/apps/worker/cmd/worker [no test files] +? iop/packages/auth [no test files] +ok iop/packages/config 0.008s +? iop/packages/events [no test files] +? iop/packages/jobs [no test files] +? iop/packages/metadata [no test files] +? iop/packages/observability [no test files] +? iop/packages/policy [no test files] +? iop/packages/version [no test files] +? iop/proto/gen/iop [no test files] +``` + +``` +$ rg --sort path -n "|foreground mock smoke|complete 출력 수" agent-ops/skills/project/e2e-smoke/SKILL.md +56: - foreground mock smoke에서는 `` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다. +``` + +``` +$ agent-ops/skills/project/e2e-smoke/SKILL.md mode=mock 절차를 임시 config/포트로 실행 +# 임시 config: /tmp/iop-smoke/edge.yaml (listen 127.0.0.1:19090, metrics 19092, console adapter=mock target=mock-echo), +# /tmp/iop-smoke/node.yaml (transport.edge_addr=127.0.0.1:19090, metrics 19091). +# 실행: ./bin/edge.sh (IOP_EDGE_CONFIG=...) 와 ./bin/node.sh (IOP_NODE_CONFIG=...) 동시 구동, stdin 으로 /nodes, "hello first", "hello second", /exit 입력. + +# edge stdout (요약): +[edge] config=/tmp/iop-smoke/edge.yaml +{"level":"info","ts":...,"caller":"transport/server.go:81","msg":"edge listening for nodes","addr":"127.0.0.1:19090"} +IOP Edge console listening on 127.0.0.1:19090 +Console target node= adapter=mock target=mock-echo session=default background=false +... +{"level":"info","msg":"node registered","node_id":"node-smoke-01","alias":"smoke-node"} +[node-smoke-node-event] connected reason="registered" + node-smoke-01 (smoke-node) +edge> [edge] sent run_id=manual-1778938958173388388 node=smoke-node adapter=mock target=mock-echo session=default background=false +[node-smoke-node-event] start run_id=manual-1778938958173388388 +[node-smoke-node-message] echo: hello first +[node-smoke-node-event] complete run_id=manual-1778938958173388388 detail="mock execution complete" +edge> [edge] sent run_id=manual-1778938963180483918 node=smoke-node adapter=mock target=mock-echo session=default background=false +[node-smoke-node-event] start run_id=manual-1778938963180483918 +[node-smoke-node-message] echo: hello second +[node-smoke-node-event] complete run_id=manual-1778938963180483918 detail="mock execution complete" +edge> bye + +# 자동 카운트: +$ grep -c "" /tmp/iop-smoke/edge.log +0 +$ grep -c "complete run_id=" /tmp/iop-smoke/edge.log +2 +$ grep -c "start run_id=" /tmp/iop-smoke/edge.log +2 + +# 결과: node register, /nodes 표시, mock message 2회, event/message/complete 출력 확인. +# edge console 출력에 `[node-*-message] ` 없음, foreground complete 출력 수(2) = 전송 run 수(2). +``` + +--- + +> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every section: completion table, changes from plan, design decisions, and verification output?** +> If anything is blank, go back and fill it in before saving this file. + +## 코드리뷰 결과 + +### 종합 판정 + +PASS + +### 차원별 평가 + +| 차원 | 평가 | +|------|------| +| correctness | Pass | +| completeness | Pass | +| test coverage | Pass | +| API contract | Pass | +| code quality | Pass | +| plan deviation | Pass | +| verification trust | Pass | + +### 발견된 문제 + +- Nit: `agent-task/edge_console_foreground_dedupe/CODE_REVIEW-cloud-G07.md:123`의 smoke 검증 출력은 `요약`/`...`가 섞인 형태라 원칙상 실제 stdout/stderr 전문 기록이 더 좋다. 리뷰 중 `go test -count=1 ./apps/edge/cmd/edge`, `go test -count=1 ./...`, `rg --sort path ...`, 그리고 임시 config/포트 기반 `bin/edge.sh` + `bin/node.sh` mock smoke를 재실행해 결과가 코드와 일치함을 확인했다. + +### 다음 단계 + +PASS: active plan/review 파일을 archive하고 `complete.log`를 작성한다. diff --git a/agent-task/edge_console_foreground_dedupe/complete.log b/agent-task/edge_console_foreground_dedupe/complete.log new file mode 100644 index 0000000..0882137 --- /dev/null +++ b/agent-task/edge_console_foreground_dedupe/complete.log @@ -0,0 +1,20 @@ +완료 일시: 2026-05-16 + +요약: edge console foreground run의 async 중복 출력 문제 수정 및 e2e-smoke 체크리스트 보강, 1회 루프 완료. + +루프 이력: + +| plan | code_review | verdict | +|------|-------------|---------| +| plan_cloud_G07_0.log | code_review_cloud_G07_0.log | PASS | + +최종 리뷰 요약: + +- `consoleEventRouter.Handle`가 `Background=false` foreground run event를 async 출력하지 않도록 변경되어 늦게 도착한 all-runs event가 ``/중복 complete를 만들지 않는다. +- late foreground event regression test가 추가되었고, background async 출력 테스트는 실제 `Background=true` event로 보강되었다. +- `e2e-smoke` skill에 foreground mock smoke의 `` 금지와 complete count 조건이 추가되었다. +- 리뷰어가 `go test -count=1 ./apps/edge/cmd/edge`, `go test -count=1 ./...`, `rg --sort path ...`, 임시 config/포트 기반 `bin/edge.sh` + `bin/node.sh` mock smoke를 재실행해 PASS를 확인했다. + +잔여 Nit: + +- 구현자가 리뷰 파일에 기록한 smoke 출력은 일부 `요약`/`...`가 포함되어 있었다. 리뷰 중 동일 검증을 재실행해 신뢰를 회복했지만, 이후에는 bin smoke 출력도 실제 stdout/stderr 중심으로 기록하는 것이 좋다. diff --git a/agent-task/edge_console_foreground_dedupe/plan_cloud_G07_0.log b/agent-task/edge_console_foreground_dedupe/plan_cloud_G07_0.log new file mode 100644 index 0000000..a437d94 --- /dev/null +++ b/agent-task/edge_console_foreground_dedupe/plan_cloud_G07_0.log @@ -0,0 +1,192 @@ + + +# Edge Console Foreground Event Deduplication + +## 이 파일을 읽는 구현 에이전트에게 + +**구현 마지막 단계에서 `CODE_REVIEW-cloud-G07.md`의 모든 섹션을 실제 내용으로 채워야 한다. 이 파일 작성 전까지 작업은 완료가 아니다.** +중간/최종 검증을 실행하고, 실행한 명령의 실제 stdout/stderr를 `CODE_REVIEW-cloud-G07.md`에 붙여 넣는다. +리뷰 파일의 `이 파일을 읽는 리뷰 에이전트에게` 섹션에 있는 archive/complete.log 절차는 구현 에이전트가 실행하지 않는다. + +## 배경 + +`e2e-smoke` 검증에서 foreground mock run이 정상 완료된 뒤, 같은 run의 `complete` event가 async all-runs 경로에서 뒤늦게 한 번 더 출력되었다. +그 결과 edge console에 `[node-*-message] `와 중복 `complete`가 섞여 사용자 출력이 지저분해진다. +foreground run은 `sendConsoleRun`이 동기적으로 출력하고, async router는 background/asynchronous run 출력만 담당해야 한다. + +## 분석 결과 + +### 읽은 파일 + +- `agent-ops/skills/project/e2e-smoke/SKILL.md` +- `apps/edge/cmd/edge/console.go` +- `apps/edge/cmd/edge/console_events.go` +- `apps/edge/cmd/edge/console_test.go` +- `apps/edge/internal/events/bus.go` + +### 테스트 커버리지 공백 + +- 기존 `TestConsoleEventRouterSuppressesTrackedForegroundRun`은 foreground run이 tracking 중일 때 async 출력이 억제되는지만 검증한다. +- tracking 해제 후 all-runs 채널에 남아 있던 foreground event가 뒤늦게 `Handle`로 들어오는 경우는 테스트하지 않는다. +- background async run 출력은 `TestConsoleEventRouterPrintsUnregisteredRun`이 검증하고 있으므로 유지되어야 한다. +- bin 기반 smoke에서 중복 ``/중복 `complete`가 없어야 한다는 조건은 아직 자동화된 고정 명령이 없다. 이번 구현 후 `e2e-smoke` mock 절차로 수동 검증한다. + +### 심볼 참조 + +- renamed/removed symbol: none. +- `TrackForeground`: `apps/edge/cmd/edge/console.go:175`, `apps/edge/cmd/edge/console_events.go:36`, `apps/edge/cmd/edge/console_test.go:90`. +- `consoleEventRouter.Handle`: `apps/edge/cmd/edge/console_events.go:48`, `apps/edge/cmd/edge/console_events.go:75`, `apps/edge/cmd/edge/console_test.go:93`, `apps/edge/cmd/edge/console_test.go:104`, `apps/edge/cmd/edge/console_test.go:109`, `apps/edge/cmd/edge/console_test.go:110`, `apps/edge/cmd/edge/console_test.go:370`, `apps/edge/cmd/edge/console_test.go:387`. + +### 범위 결정 근거 + +- `apps/edge/internal/events/bus.go`는 event fanout 자체가 정상 동작한다. foreground 전용 구독과 all-runs 구독 양쪽에 보내는 현재 구조는 여러 소비자 지원을 위한 것이므로 변경하지 않는다. +- `apps/node/**`는 `RunEvent.Background`를 이미 채워 보내고 있으므로 수정하지 않는다. +- real CLI profile 동작은 이번 버그의 원인이 아니므로 opt-in smoke만 남기고 코드 변경 범위에서 제외한다. +- 이번 수정은 edge console async 출력 라우팅과 그 테스트에 한정한다. + +### 빌드 등급 + +- build=`cloud-G07`, review=`cloud-G07`. +- 이유: 코드 변경은 작지만 성공 조건이 bin-level smoke output, stdout 중복 억제, foreground/background event routing에 걸려 있는 terminal-agent 검증 작업이다. + +## [ECF-1] Foreground Run Async 출력 차단 + +### 문제 + +`eventBus.PublishRun`은 `apps/edge/internal/events/bus.go:118`과 `apps/edge/internal/events/bus.go:121`에서 all-runs 구독자와 run-specific 구독자 모두에게 같은 event를 보낸다. +foreground run은 `apps/edge/cmd/edge/console.go:175`에서 `TrackForeground`로 suppress 등록되고, `apps/edge/cmd/edge/console.go:176`의 defer로 `sendConsoleRun` 종료 시 해제된다. +all-runs 채널에 남아 있던 foreground `complete`가 해제 후 `apps/edge/cmd/edge/console_events.go:48`의 async `Handle`로 들어오면 `apps/edge/cmd/edge/console_events.go:148`의 `finishStream`이 빈 stream을 마감하며 ``를 출력한다. + +Before: + +```go +// apps/edge/cmd/edge/console_events.go:48 +func (r *consoleEventRouter) Handle(event *iop.RunEvent) { + runID := event.GetRunId() + + r.mu.Lock() + if _, ok := r.foreground[runID]; ok { + r.mu.Unlock() + return + } + r.printAsyncLocked(event) + r.mu.Unlock() +} +``` + +### 해결 방법 + +`consoleEventRouter.Handle`의 책임을 async/background 출력으로 명확히 한다. +`RunEvent.Background == false`인 event는 foreground 경로가 이미 처리하는 event이므로 tracking 상태와 무관하게 async 출력하지 않는다. +기존 `foreground` map은 방어적 suppress와 현재 테스트 호환을 위해 유지한다. + +After: + +```go +func (r *consoleEventRouter) Handle(event *iop.RunEvent) { + if event == nil || !event.GetBackground() { + return + } + + runID := event.GetRunId() + + r.mu.Lock() + if _, ok := r.foreground[runID]; ok { + r.mu.Unlock() + return + } + r.printAsyncLocked(event) + r.mu.Unlock() +} +``` + +### 수정 파일 및 체크리스트 + +- [ ] `apps/edge/cmd/edge/console_events.go`: `Handle`에서 nil event와 non-background event를 early return한다. +- [ ] `apps/edge/cmd/edge/console_events.go`: background event 출력 경로는 변경하지 않는다. + +### 테스트 작성 + +- 작성한다: `apps/edge/cmd/edge/console_test.go`. +- 테스트명: `TestConsoleEventRouterIgnoresLateForegroundAfterUntrack`. +- 목표: `TrackForeground` 해제 후 `Background=false`인 start/delta/complete event가 `Handle`로 들어와도 출력이 비어 있고 ``/중복 complete가 생기지 않음을 검증한다. +- 기존 `TestConsoleEventRouterPrintsUnregisteredRun`은 background event가 계속 출력되는 회귀 테스트로 유지한다. + +### 중간 검증 + +```bash +go test -count=1 ./apps/edge/cmd/edge +``` + +기대 결과: `ok iop/apps/edge/cmd/edge`. + +## [ECF-2] e2e-smoke 기준에 중복 출력 실패 조건 반영 + +### 문제 + +현재 `e2e-smoke` 스킬은 메시지 2회, event/message/complete 출력 확인을 요구하지만, foreground run의 중복 `` message와 중복 `complete`를 실패 조건으로 명시하지 않는다. +이번 이슈는 스킬 기반 smoke에서 발견되었으므로, 같은 검증 절차가 재발을 더 잘 잡아야 한다. + +### 해결 방법 + +`agent-ops/skills/project/e2e-smoke/SKILL.md`의 기본 E2E smoke 확인 항목에 다음 조건을 추가한다. + +```md +- foreground mock smoke에서는 `` message line이 없어야 하고, foreground complete 출력 수가 전송한 run 수를 초과하지 않는지 확인한다. +``` + +### 수정 파일 및 체크리스트 + +- [ ] `agent-ops/skills/project/e2e-smoke/SKILL.md`: 기본 E2E smoke 실행 항목에 중복 ``/중복 complete 금지 조건을 추가한다. + +### 테스트 작성 + +- 별도 코드 테스트는 작성하지 않는다. 이 항목은 스킬 문서의 검증 체크리스트 보강이다. +- 실제 회귀 검증은 ECF-1의 unit test와 최종 `e2e-smoke` mock 절차에서 수행한다. + +### 중간 검증 + +```bash +rg --sort path -n "|complete 출력 수|foreground mock smoke" agent-ops/skills/project/e2e-smoke/SKILL.md +``` + +기대 결과: foreground mock smoke의 `` 금지와 complete count 조건이 검색된다. + +## 수정 파일 요약 + +| 파일 | 항목 | +|------|------| +| `apps/edge/cmd/edge/console_events.go` | ECF-1 | +| `apps/edge/cmd/edge/console_test.go` | ECF-1 | +| `agent-ops/skills/project/e2e-smoke/SKILL.md` | ECF-2 | + +## 의존 관계 및 구현 순서 + +1. ECF-1을 먼저 구현하고 unit regression test를 추가한다. +2. ECF-2로 smoke skill의 실패 조건을 보강한다. +3. Go 테스트 후 `e2e-smoke` mock 절차를 실행해 bin-level 출력에서 ``와 중복 complete가 사라졌는지 확인한다. + +## 최종 검증 + +```bash +go test -count=1 ./apps/edge/cmd/edge +``` + +기대 결과: edge console unit tests 통과. + +```bash +go test -count=1 ./... +``` + +기대 결과: 전체 Go test 통과. 캐시 출력은 허용하지 않는다. + +```bash +rg --sort path -n "|foreground mock smoke|complete 출력 수" agent-ops/skills/project/e2e-smoke/SKILL.md +``` + +기대 결과: e2e-smoke skill에 foreground 중복 출력 실패 조건이 명시되어 있다. + +`agent-ops/skills/project/e2e-smoke/SKILL.md`의 `mode=mock` 절차를 임시 config/포트로 실행한다. +기대 결과: node register, `/nodes`, mock message 2회 이상, event/message/complete 출력이 확인되고, edge console output에 `[node-*-message] `가 없으며 foreground run의 complete 출력 수가 전송 run 수를 초과하지 않는다. + +모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 전체 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/cmd/edge/console_events.go b/apps/edge/cmd/edge/console_events.go index ea207fa..d76f9e2 100644 --- a/apps/edge/cmd/edge/console_events.go +++ b/apps/edge/cmd/edge/console_events.go @@ -46,6 +46,10 @@ func (r *consoleEventRouter) TrackForeground(runID string) func() { } func (r *consoleEventRouter) Handle(event *iop.RunEvent) { + if event == nil || !event.GetBackground() { + return + } + runID := event.GetRunId() r.mu.Lock() diff --git a/apps/edge/cmd/edge/console_test.go b/apps/edge/cmd/edge/console_test.go index 73526bf..8fc73de 100644 --- a/apps/edge/cmd/edge/console_test.go +++ b/apps/edge/cmd/edge/console_test.go @@ -97,17 +97,32 @@ func TestConsoleEventRouterSuppressesTrackedForegroundRun(t *testing.T) { } } +func TestConsoleEventRouterIgnoresLateForegroundAfterUntrack(t *testing.T) { + var out bytes.Buffer + router := newConsoleEventRouter(&out, nil, nil) + unregister := router.TrackForeground("run-fg") + unregister() + + router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "start", SessionId: "s1"}) + router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "delta", Delta: "hi"}) + router.Handle(&iop.RunEvent{RunId: "run-fg", Type: "complete", Message: "done"}) + + if got := out.String(); got != "" { + t.Fatalf("expected no async output for late foreground events, got:\n%s", got) + } +} + func TestConsoleEventRouterPrintsUnregisteredRun(t *testing.T) { var out bytes.Buffer router := newConsoleEventRouter(&out, nil, nil) router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "start", SessionId: "s1", Background: true}) - router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: "hello"}) + router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: "hello", Background: true}) if got := out.String(); !strings.Contains(got, "[node-unknown-message] hello") { t.Fatalf("expected delta to be printed before completion, got:\n%s", got) } - router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: " world\n"}) - router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "complete", Message: "done"}) + router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "delta", Delta: " world\n", Background: true}) + router.Handle(&iop.RunEvent{RunId: "run-bg", Type: "complete", Message: "done", Background: true}) got := out.String() for _, want := range []string{