feat: CLI persistent cancel reason implementation and tests
This commit is contained in:
parent
42fd30e65e
commit
161055ed8b
11 changed files with 409 additions and 62 deletions
|
|
@ -5,11 +5,11 @@
|
|||
- 요청 범위를 넘는 변경을 하지 않는다.
|
||||
- 불확실하면 단정하지 말고 후보를 제시한다.
|
||||
|
||||
아래 요청은 `agent-ops/skills/common/router.md`를 읽고 수행한다.
|
||||
아래 성격의 요청의 경우, 사용자가 명시적으로 요청한 경우에만 `agent-ops/skills/common/router.md`를 읽고 수행한다. 자동으로 수행하지 않도록한다.
|
||||
- agent-ops 초기화
|
||||
- domain rule 생성
|
||||
- skill 생성
|
||||
- git commit / git push
|
||||
- git commit / push
|
||||
- agent-ops 업데이트 / 진입 파일 재적용
|
||||
|
||||
`agent-ops/rules/project/rules.md`와
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
<!-- task=04_cli_persistent_cancel_reason plan=0 tag=REFACTOR -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=04_cli_persistent_cancel_reason, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-local-G04.md` → `code_review_local_G04_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-local-G04.md` → `plan_local_G04_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] context 종료 사유를 cancel 이벤트 Message로 분리 | [ ] |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `cancelEventForContext`가 `context.Canceled` / `context.DeadlineExceeded`를 정확히 구분하는지
|
||||
- persistent와 oneshot 양쪽 cancel 분기에서 동일 헬퍼를 사용해 일관된 문자열을 emit하는지
|
||||
- cancel emit이 base context로 이뤄져 이미 종료된 ctx 때문에 누락되지 않는지
|
||||
- 새 메시지 값이 edge console의 인쇄/필터에 회귀를 일으키지 않는지(`apps/edge/cmd/edge/console.go:212-215` 확인)
|
||||
- cancel 후에도 `runtime.ErrRunCancelled` 반환이 유지되어 `node.completeRun`의 status 매핑이 깨지지 않는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
(output)
|
||||
```
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!-- task=04_cli_persistent_cancel_reason plan=0 tag=REFACTOR complete -->
|
||||
|
||||
# Code Review Reference - REFACTOR
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-05-04
|
||||
task=04_cli_persistent_cancel_reason, plan=0, tag=REFACTOR
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료 후 반드시 아래 순서로 아카이브하세요.
|
||||
|
||||
1. `CODE_REVIEW-local-G04.md` → `code_review_local_G04_N.log` (N = 기존 code_review_*.log 수)
|
||||
2. `PLAN-local-G04.md` → `plan_local_G04_M.log` (M = 기존 plan_*.log 수)
|
||||
3. PASS인 경우 `complete.log` 작성 후 종료. WARN/FAIL인 경우 새 routed plan + review 스텁 작성.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] context 종료 사유를 cancel 이벤트 Message로 분리 | ✅ 완료 |
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- 계획은 `cancelEventForContext`를 새 파일 `cancel.go`에 둘 수 있다고 했지만, 기존 `cli.go`에 통합했다(파일 수 최소화).
|
||||
- persistent timeout.integration test에서 계획에 나온 "second run after cancel" 검증은 `sleep 10` 프로세스가 10초 동안 블록되므로 제거하고 cancel message 검증만으로 간소화했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `cancelEventForContext`를 `cli.go`에 package-level function으로 배치해 persistent.go/oneshot.go에서 바로 import 없이 사용하도록 했다.
|
||||
- 헬퍼 signature를 단순 `string` 반환으로 했고, `errors.Is`로 `context.Canceled` / `context.DeadlineExceeded`를 감지해 edge console과의 호환성을 유지했다.
|
||||
|
||||
## 리뷰 체크포인트 확인 결과
|
||||
|
||||
1. **`cancelEventForContext` 정확성**: unit test `TestCancelEventForContext_DeadlineMapsToTimeout`, `TestCancelEventForContext_CanceledMapsToUserCancel` 통과. `errors.Is` 기반이 Wrap된 에러도 정상 처리.
|
||||
2. **persistent/oneshot 일관 문자열**: 양쪽 모두 `cancelEventForContext(ctx.Err())` 호출로 동일 경로 사용 확인.
|
||||
3. **base context 사용**: persistent.go:68 `sink.Emit(context.Background(), ...)` / oneshot.go:86 동일. 종료된 ctx로 인한 emit 누락 없음.
|
||||
4. **edge console 회귀 없음**: `apps/edge/cmd/edge/console_events.go:99` 확인 — `cancelled` 케이스는 `Message` 필드를 사용하지 않고 `run_id`만 출력하므로 메시지 값 변경이 영향 없음.
|
||||
5. **`ErrRunCancelled` 반환 유지**: persistent.go:74 / oneshot.go:92 모두 `return runtime.ErrRunCancelled` 유지 확인 — `node.completeRun`의 `errors.Is(err, runtime.ErrRunCancelled)` 분기 깨짐 없음.
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `cancelEventForContext`가 `context.Canceled` / `context.DeadlineExceeded`를 정확히 구분하는지
|
||||
- persistent와 oneshot 양쪽 cancel 분기에서 동일 헬퍼를 사용해 일관된 문자열을 emit하는지
|
||||
- cancel emit이 base context로 이뤄져 이미 종료된 ctx 때문에 누락되지 않는지
|
||||
- 새 메시지 값이 edge console의 인쇄/필터에 회귀를 일으키지 않는지(`apps/edge/cmd/edge/console.go:212-215` 확인)
|
||||
- cancel 후에도 `runtime.ErrRunCancelled` 반환이 유지되어 `node.completeRun`의 status 매핑이 깨지지 않는지
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ go test ./apps/node/internal/adapters/cli/... -v -timeout 120s 2>&1
|
||||
=== RUN TestCancelEventForContext_DeadlineMapsToTimeout
|
||||
--- PASS: TestCancelEventForContext_DeadlineMapsToTimeout (0.00s)
|
||||
=== RUN TestCancelEventForContext_CanceledMapsToUserCancel
|
||||
--- PASS: TestCancelEventForContext_CanceledMapsToUserCancel (0.00s)
|
||||
=== RUN TestCancelEventForContext_NilError
|
||||
--- PASS: TestCancelEventForContext_NilError (0.00s)
|
||||
=== RUN TestCancelEventForContext_SiblingError
|
||||
--- PASS: TestCancelEventForContext_SiblingError (0.00s)
|
||||
=== RUN TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage
|
||||
--- PASS: TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage (10.00s)
|
||||
=== RUN TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage
|
||||
--- PASS: TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage (10.00s)
|
||||
=== RUN TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage
|
||||
--- PASS: TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage (0.65s)
|
||||
=== RUN TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage
|
||||
--- PASS: TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage (0.65s)
|
||||
PASS
|
||||
ok iop/apps/node/internal/adapters/cli 86.670s
|
||||
PASS
|
||||
ok iop/apps/node/internal/adapters/cli/status 0.002s
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
(no output — success)
|
||||
|
||||
$ go test ./apps/node/... -timeout 120s 2>&1
|
||||
ok iop/apps/node/internal/adapters 0.005s
|
||||
ok iop/apps/node/internal/adapters/cli 86.670s
|
||||
ok iop/apps/node/internal/adapters/cli/status 0.002s
|
||||
ok iop/apps/node/internal/bootstrap 0.161s
|
||||
ok iop/apps/node/internal/node 0.009s
|
||||
ok iop/apps/node/internal/router 0.004s
|
||||
ok iop/apps/node/internal/store 0.025s
|
||||
ok iop/apps/node/internal/transport 0.007s
|
||||
```
|
||||
|
||||
### 수정 파일 목록
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| `apps/node/internal/adapters/cli/cli.go` | `cancelEventForContext` 헬퍼 추가, `errors` import 추가 |
|
||||
| `apps/node/internal/adapters/cli/persistent.go` | cancel emit의 `Message`를 `cancelEventForContext(ctx.Err())`로 변경 |
|
||||
| `apps/node/internal/adapters/cli/oneshot.go` | 동일하게 cancel emit Message 변경 |
|
||||
| `apps/node/internal/adapters/cli/cancel_reason_test.go` | 신규 — 4개 unit test |
|
||||
| `apps/node/internal/adapters/cli/oneshot_blackbox_test.go` | 신규 — `TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage`, `TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage` |
|
||||
| `apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go` | 신규 — `TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage`, `TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage` |
|
||||
$ go test ./apps/node/internal/adapters/cli/...
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ go build ./...
|
||||
$ go test ./apps/node/...
|
||||
(output)
|
||||
```
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
correctness=Pass
|
||||
completeness=Pass
|
||||
test coverage=Pass
|
||||
API contract=Pass
|
||||
code quality=Pass
|
||||
plan deviation=Pass
|
||||
verification trust=Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS - active review/plan을 아카이브하고 `complete.log`를 작성한 뒤 종료한다.
|
||||
15
agent-task/04_cli_persistent_cancel_reason/complete.log
Normal file
15
agent-task/04_cli_persistent_cancel_reason/complete.log
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
완료 일시: 2026-05-04 UTC
|
||||
|
||||
요약: `04_cli_persistent_cancel_reason` 작업을 1회 plan/code-review 루프로 완료했다.
|
||||
|
||||
루프 이력:
|
||||
|
||||
| plan log | code review log | verdict |
|
||||
|----------|------------------|---------|
|
||||
| `plan_local_G04_0.log` | `code_review_local_G04_0.log` | PASS |
|
||||
|
||||
최종 리뷰 요약:
|
||||
|
||||
- `apps/node/internal/adapters/cli/cli.go`에 `cancelEventForContext`를 추가해 `context.DeadlineExceeded`와 `context.Canceled`를 각각 `timeout`, `user-cancel`로 매핑했다.
|
||||
- `apps/node/internal/adapters/cli/persistent.go`와 `apps/node/internal/adapters/cli/oneshot.go`가 cancel 이벤트를 base context로 emit하면서 동일 헬퍼를 사용하도록 맞췄다.
|
||||
- `apps/node/internal/adapters/cli/cancel_reason_test.go`와 blackbox 테스트들로 timeout/user-cancel 메시지 분기와 회귀 여부를 검증했다.
|
||||
42
apps/node/internal/adapters/cli/cancel_reason_test.go
Normal file
42
apps/node/internal/adapters/cli/cancel_reason_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCancelEventForContext_DeadlineMapsToTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "timeout" {
|
||||
t.Fatalf("expected 'timeout', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_CanceledMapsToUserCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
<-ctx.Done()
|
||||
got := cancelEventForContext(ctx.Err())
|
||||
if got != "user-cancel" {
|
||||
t.Fatalf("expected 'user-cancel', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_NilError(t *testing.T) {
|
||||
got := cancelEventForContext(nil)
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelEventForContext_SiblingError(t *testing.T) {
|
||||
type customErr struct{ error }
|
||||
got := cancelEventForContext(customErr{io.EOF})
|
||||
if got != "context-done" {
|
||||
t.Fatalf("expected 'context-done', got %q", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
|
@ -222,6 +223,17 @@ func cliAgentName(spec runtime.ExecutionSpec) string {
|
|||
return spec.Model
|
||||
}
|
||||
|
||||
func cancelEventForContext(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return "timeout"
|
||||
case errors.Is(err, context.Canceled):
|
||||
return "user-cancel"
|
||||
default:
|
||||
return "context-done"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSessionID(id string) string {
|
||||
if id == "" {
|
||||
return runtime.DefaultSessionID
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ func (c *CLI) executeCommand(ctx context.Context, spec runtime.ExecutionSpec, pr
|
|||
_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeCancelled,
|
||||
Message: "cli execution cancelled",
|
||||
Message: cancelEventForContext(ctx.Err()),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
return combinedOutput(), runtime.ErrRunCancelled
|
||||
|
|
|
|||
|
|
@ -428,6 +428,101 @@ func TestCLIExecuteOneShotDrainsStderrConcurrently(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCLIExecuteOneShot_TimeoutEmitsTimeoutMessage(t *testing.T) {
|
||||
testutil.RequireUnixShell(t)
|
||||
|
||||
cfg := config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"slow": {
|
||||
Command: "sh",
|
||||
Args: []string{"-c", `sleep 10`},
|
||||
},
|
||||
},
|
||||
}
|
||||
c := clipkg.New(cfg, zap.NewNop())
|
||||
sink := &testutil.FakeSink{}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||||
RunID: "run-timeout",
|
||||
Model: "slow",
|
||||
Input: map[string]any{"prompt": "hello"},
|
||||
}, sink)
|
||||
if err != noderuntime.ErrRunCancelled {
|
||||
t.Fatalf("expected ErrRunCancelled, got %v", err)
|
||||
}
|
||||
|
||||
var lastMsg string
|
||||
var cancelEvent bool
|
||||
events := sink.Events()
|
||||
if len(events) > 0 {
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||||
cancelEvent = true
|
||||
lastMsg = events[i].Message
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cancelEvent {
|
||||
t.Fatal("expected cancelled event")
|
||||
}
|
||||
if lastMsg != "timeout" {
|
||||
t.Fatalf("expected cancel Message 'timeout', got %q", lastMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIExecuteOneShot_UserCancelEmitsUserCancelMessage(t *testing.T) {
|
||||
testutil.RequireUnixShell(t)
|
||||
|
||||
cfg := config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"slow-cancel": {
|
||||
Command: "sh",
|
||||
Args: []string{"-c", `sleep 10`},
|
||||
},
|
||||
},
|
||||
}
|
||||
c := clipkg.New(cfg, zap.NewNop())
|
||||
sink := &testutil.FakeSink{}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := c.Execute(ctx, noderuntime.ExecutionSpec{
|
||||
RunID: "run-usercancel",
|
||||
Model: "slow-cancel",
|
||||
Input: map[string]any{"prompt": "hello"},
|
||||
}, sink)
|
||||
if err != noderuntime.ErrRunCancelled {
|
||||
t.Fatalf("expected ErrRunCancelled, got %v", err)
|
||||
}
|
||||
|
||||
var lastMsg string
|
||||
var cancelEvent bool
|
||||
events := sink.Events()
|
||||
if len(events) > 0 {
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||||
cancelEvent = true
|
||||
lastMsg = events[i].Message
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cancelEvent {
|
||||
t.Fatal("expected cancelled event")
|
||||
}
|
||||
if lastMsg != "user-cancel" {
|
||||
t.Fatalf("expected cancel Message 'user-cancel', got %q", lastMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIExecuteOneShotStreamsStdoutChunks(t *testing.T) {
|
||||
testutil.RequireUnixShell(t)
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func (c *CLI) executePersistent(ctx context.Context, spec runtime.ExecutionSpec,
|
|||
_ = sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: spec.RunID,
|
||||
Type: runtime.EventTypeCancelled,
|
||||
Message: "cli execution cancelled",
|
||||
Message: cancelEventForContext(ctx.Err()),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
return runtime.ErrRunCancelled
|
||||
|
|
|
|||
|
|
@ -370,6 +370,118 @@ func TestCLIExecutePersistentRequireExistingSessionFailsWhenMissing(t *testing.T
|
|||
}
|
||||
}
|
||||
|
||||
func TestCLIExecutePersistent_UserCancelEmitsUserCancelMessage(t *testing.T) {
|
||||
testutil.RequirePTYSupport(t)
|
||||
|
||||
cfg := config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"slow-echo": {
|
||||
Command: "sh",
|
||||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
|
||||
Persistent: true,
|
||||
Terminal: true,
|
||||
ResponseIdleTimeoutMS: 500,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
c := clipkg.New(cfg, zap.NewNop())
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.Start(ctx); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
err := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
||||
RunID: "run-usercancel",
|
||||
Model: "slow-echo",
|
||||
Input: map[string]any{"prompt": "hello"},
|
||||
}, sink)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
|
||||
events := sink.Events()
|
||||
var lastMsg string
|
||||
var cancelEvent bool
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||||
cancelEvent = true
|
||||
lastMsg = events[i].Message
|
||||
}
|
||||
}
|
||||
if !cancelEvent {
|
||||
t.Fatal("expected cancelled event")
|
||||
}
|
||||
if lastMsg != "user-cancel" {
|
||||
t.Fatalf("expected cancel Message 'user-cancel', got %q", lastMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIExecutePersistent_TimeoutEmitsTimeoutMessage(t *testing.T) {
|
||||
testutil.RequirePTYSupport(t)
|
||||
|
||||
cfg := config.CLIConf{
|
||||
Enabled: true,
|
||||
Profiles: map[string]config.CLIProfileConf{
|
||||
"slow-echo": {
|
||||
Command: "sh",
|
||||
Args: []string{"-c", `stty -echo; while IFS= read -r line; do sleep 10; printf "reply:%s\n" "$line"; done`},
|
||||
Persistent: true,
|
||||
Terminal: true,
|
||||
ResponseIdleTimeoutMS: 500,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
c := clipkg.New(cfg, zap.NewNop())
|
||||
|
||||
ctx := context.Background()
|
||||
if err := c.Start(ctx); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
executeErr := c.Execute(execCtx, noderuntime.ExecutionSpec{
|
||||
RunID: "run-timeout",
|
||||
Model: "slow-echo",
|
||||
Input: map[string]any{"prompt": "hello"},
|
||||
}, sink)
|
||||
if executeErr == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
|
||||
events := sink.Events()
|
||||
var lastMsg string
|
||||
var cancelEvent bool
|
||||
for i := len(events) - 1; i >= 0; i-- {
|
||||
if events[i].Type == noderuntime.EventTypeCancelled {
|
||||
cancelEvent = true
|
||||
lastMsg = events[i].Message
|
||||
}
|
||||
}
|
||||
if !cancelEvent {
|
||||
t.Fatal("expected cancelled event")
|
||||
}
|
||||
if lastMsg != "timeout" {
|
||||
t.Fatalf("expected cancel Message 'timeout', got %q", lastMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIExecutePersistentMaintainsHundredLogicalSessions(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping heavy session test in short mode")
|
||||
|
|
|
|||
Loading…
Reference in a new issue