iop/agent-task/04_cli_persistent_cancel_reason/PLAN.md

4.7 KiB

Persistent 실행 cancel 사유(timeout vs user-cancel) 구분

이 파일을 읽는 구현 에이전트에게

아래 체크리스트를 순서대로 완료하고, 각 항목의 중간 검증과 최종 검증을 실제로 실행하세요. 구현이 끝나면 agent-task/04_cli_persistent_cancel_reason/CODE_REVIEW.md의 모든 섹션을 실제 구현 내용과 명령 출력으로 채우세요. CODE_REVIEW.md이 파일을 읽는 리뷰 에이전트에게 섹션에 있는 아카이브 지시(*.log로 이름 변경, complete.log 작성)는 구현 에이전트가 수행하면 안 되며, 리뷰 스킬 전용입니다.

배경

apps/node/internal/adapters/cli/persistent.go:65-74<-ctx.Done() 분기와 apps/node/internal/adapters/cli/oneshot.go:96-104의 동일 분기는 timeout으로 인한 컨텍스트 종료와 사용자 cancel을 구분하지 않고 모두 EventTypeCancelled + Message: "cli execution cancelled"로 보고합니다. 운영 시 두 사유는 대응이 다르므로 (timeout은 설정 조정, user-cancel은 워크플로 변경) Message/Error 필드에 사유를 명시해 구분 가능하게 합니다.

[REFACTOR-1] context 종료 사유를 cancel 이벤트에 반영

문제

  • persistent.go:65-74: case <-ctx.Done():에서 항상 Message: "cli execution cancelled"로 emit.
  • oneshot.go:96-103: 동일하게 Message: "cli execution cancelled".

context.Canceledcontext.DeadlineExceeded를 구분하지 않는다.

해결 방법

각 분기에서 ctx.Err()를 보고 사유를 분기한다.

func cancelEventForContext(err error) (msg string) {
    switch {
    case errors.Is(err, context.DeadlineExceeded):
        return "timeout"
    case errors.Is(err, context.Canceled):
        return "user-cancel"
    default:
        return "context-done"
    }
}

persistent.go:

case <-ctx.Done():
    drainSessionUntilIdle(sess.output, idleTimeout, c.logger, sess.key)
    _ = sink.Emit(context.Background(), runtime.RuntimeEvent{
        RunID:     spec.RunID,
        Type:      runtime.EventTypeCancelled,
        Message:   cancelEventForContext(ctx.Err()),
        Timestamp: time.Now(),
    })
    return runtime.ErrRunCancelled

oneshot.gocmd.Wait() 실패 후 if ctx.Err() != nil 분기도 동일하게:

_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
    RunID:     spec.RunID,
    Type:      runtime.EventTypeCancelled,
    Message:   cancelEventForContext(ctx.Err()),
    Timestamp: time.Now(),
})
return combinedOutput(), runtime.ErrRunCancelled

cancelEventForContext 헬퍼는 apps/node/internal/adapters/cli/cli.go 또는 새 파일 apps/node/internal/adapters/cli/cancel.go에 둔다. 본 계획은 cli.go에 둔다.

호환성: edge console (apps/edge/cmd/edge/console.go:212-215)이 cancel 이벤트의 Message를 인쇄에만 사용하므로 동작 깨짐 없음. 다만 노출되는 텍스트가 바뀌므로 사용자 안내 문서/리드미는 따로 갱신하지 않는다(현재 README가 해당 문구를 명시하지 않음).

수정 파일 및 체크리스트

  • apps/node/internal/adapters/cli/cli.gocancelEventForContext 헬퍼를 추가한다.
  • apps/node/internal/adapters/cli/persistent.goctx.Done() 분기 Message를 헬퍼 호출로 교체한다.
  • apps/node/internal/adapters/cli/oneshot.go의 cancel emit Message를 헬퍼 호출로 교체한다.
  • errors/context import가 두 파일에 모두 있는지 확인한다.

테스트 작성

  • TestCancelEventForContext_DeadlineMapsToTimeout: cancelEventForContext(context.DeadlineExceeded) == "timeout" 단언.
  • TestCancelEventForContext_CanceledMapsToUserCancel: context.Canceled"user-cancel" 단언.
  • TestExecutePersistent_TimeoutEmitsTimeoutMessage: 짧은 deadline ctx로 persistent 실행 → 마지막 cancel 이벤트의 Message == "timeout" 단언.
  • TestExecuteOneShot_UserCancelEmitsUserCancelMessage: 명시적 cancel() 호출 후 cancel 이벤트 Message == "user-cancel" 단언.

중간 검증

go test ./apps/node/internal/adapters/cli/...

예상 결과: 신규 케이스 PASS.

수정 파일 요약

파일 항목
apps/node/internal/adapters/cli/cli.go REFACTOR-1
apps/node/internal/adapters/cli/persistent.go REFACTOR-1
apps/node/internal/adapters/cli/oneshot.go REFACTOR-1
apps/node/internal/adapters/cli/lifecycle_blackbox_test.go 또는 적절한 위치 REFACTOR-1

최종 검증

go build ./...
go test ./apps/node/...

예상 결과: 모든 테스트 PASS, cancel 이벤트의 Messagetimeout / user-cancel / context-done 셋 중 하나로 emit됨.