refactor: repository readable restructuring - OpenAI tests, roadmap, and cleanup
- Add unit tests for OpenAI edge adapter (chat handler, stream reasoning, tool synthesis, auth routes, test support) - Add roadmap: agent-readable-repository-refactor milestone with subtask plans - Update PHASE.md, priority-queue.md, .gitignore - Delete old edge/node/openai.test files (replaced by proper test structure)
This commit is contained in:
parent
49ecb3ec9e
commit
8c5b01c893
40 changed files with 6991 additions and 3190 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,6 +15,11 @@ agent-test/runs/
|
|||
/.env
|
||||
/temp_verif/
|
||||
|
||||
# Legacy root build outputs (official targets write to /build/)
|
||||
/node
|
||||
/edge
|
||||
/openai.test
|
||||
|
||||
# Flutter Portal artifacts
|
||||
apps/portal/.dart_tool/
|
||||
apps/portal/build/
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ Phase를 가로지르는 실제 다음 작업 선택은 [전역 마일스톤 실
|
|||
- 경로: [openai-workspace-agent-execution-contract](../../archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md)
|
||||
- 요약: NomadCode가 IOP CLI를 직접 실행하지 않고 IOP Edge OpenAI-compatible HTTP 호출의 `metadata.workspace`와 task/source metadata만으로 내부 workspace-bound agent target이 해당 checkout에서 산출물을 만들 수 있게 하는 최우선 contract/serving hardening 작업이다.
|
||||
|
||||
- [진행중] 에이전트 작업성 중심 저장소 구조 리팩터링
|
||||
- 경로: [agent-readable-repository-refactor](milestones/agent-readable-repository-refactor.md)
|
||||
- 요약: 거대 소스·테스트·Agent-Ops 문서와 추적 artifact를 책임 단위로 정리해 task-local read set을 줄이고 동작 보존형 가독성 기준선을 만든다.
|
||||
|
||||
- [계획] Pi CLI Provider Integration
|
||||
- 경로: [pi-cli-provider-integration](milestones/pi-cli-provider-integration.md)
|
||||
- 요약: Node CLI adapter의 실행 profile 후보에 Pi를 추가하고, Pi JSON stream 출력 파서, config 예시, OpenAI-compatible route smoke를 통해 `adapter=cli,target=pi`를 안정적으로 사용할 수 있게 한다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
# Milestone: 에이전트 작업성 중심 저장소 구조 리팩터링
|
||||
|
||||
## 위치
|
||||
|
||||
- Roadmap: [ROADMAP.md](../../../ROADMAP.md)
|
||||
- Phase: [PHASE.md](../PHASE.md)
|
||||
|
||||
## 목표
|
||||
|
||||
생성물과 대형 데이터 파일을 제외한 사람이 관리하는 소스, 테스트, Agent-Ops 문서를 책임과 변경 이유가 드러나는 단위로 재구성한다.
|
||||
기존 API, wire, config schema와 런타임 동작을 유지하면서 에이전트가 한 기능을 수정하기 위해 읽어야 하는 파일 수와 task-local context를 줄이고, 이후 변경에서 다시 거대 파일이 생기지 않도록 측정 가능한 기준선을 둔다.
|
||||
|
||||
## 상태
|
||||
|
||||
[진행중]
|
||||
|
||||
## 승격 조건
|
||||
|
||||
- 없음
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
- 상태: 해제
|
||||
- SDD: 불필요
|
||||
- SDD 문서: 없음
|
||||
- SDD 사유: 외부 API/proto/config schema나 runtime 책임 경계를 바꾸지 않고 동일 패키지 내 책임 분리, 테스트 재배치, Agent-Ops 문서 라우팅 정리와 저장소 위생 개선을 수행하는 동작 보존형 리팩터링이다.
|
||||
- 잠금 해제 조건: 없음
|
||||
- 결정 필요: 없음
|
||||
|
||||
## 범위
|
||||
|
||||
- 추적 중인 빌드 바이너리와 파일 길이 측정 노이즈를 정리하고, 생성물·lockfile·PBX·대형 fixture 예외를 구분하는 저장소 가독성 기준선을 둔다.
|
||||
- `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/cmd/edge`, `apps/node/internal/node`, `apps/node/internal/adapters`, `apps/control-plane/cmd/control-plane`, `packages/go/config`, Flutter client와 E2E script의 거대 파일을 책임과 변경 이유 기준으로 분해한다.
|
||||
- 거대 테스트 파일을 기능 시나리오별로 나누고, 만능 fake와 반복 fixture setup을 feature-local stub/helper로 정리한다.
|
||||
- 120줄 이상 장기 함수, 과도한 parameter list와 closure 기반 상태 처리를 명명된 context/session 객체와 단계별 함수 후보로 정리한다.
|
||||
- Agent-Ops Skill 진입 문서와 agent-test inventory의 전체 읽기 비용을 줄이되 현재 라우팅 깊이, plan/code-review 짝 계약과 실행 의미를 보존한다.
|
||||
- 파일 이동 뒤 domain rule, agent-spec source evidence와 현재 계약 포인터가 실제 구현 경로를 계속 가리키도록 동기화한다.
|
||||
|
||||
## 기능
|
||||
|
||||
### Epic: [hygiene] 저장소 노이즈와 가독성 기준선
|
||||
|
||||
에이전트 탐색을 오염시키는 artifact를 제거하고 신규 구조 부채를 조기에 확인할 수 있는 기준을 만든다.
|
||||
|
||||
- [x] [tracked-artifacts] 루트의 추적 중 ELF 빌드 산출물 `node`, `edge`, `openai.test`를 Git 추적에서 제거하고 공식 빌드 산출 위치와 ignore 규칙을 정렬한다. Git history 재작성은 수행하지 않는다. 검증: `git ls-files node edge openai.test`가 비어 있고 공식 build target이 `build/` 아래 산출물을 만든다.
|
||||
- [ ] [readability-baseline] 생성물과 data-only 예외를 제외한 파일 LOC, 함수 크기와 task-local read set을 점검하는 기준선과 ratchet 정책을 둔다. 기본 code/test 입력은 Git이 추적하는 Go/Dart/Shell/Python/Kotlin/Swift 파일로 정의하고 protobuf·Dart 생성 경로는 제외한다. 운영 파일 500줄 경고/800줄 분리 검토/1,000줄 예외, 테스트 800줄 경고/1,000줄 분리 검토, 함수 80줄 경고/120줄 분리 검토, Skill 진입 파일 300줄 경고/500줄 분리 검토를 기본값으로 기록한다. 검증: 동일 입력에서 재현 가능한 audit 결과가 생성되고 신규 초과 또는 기존 초과 증가가 실패하거나 명시적 allowlist 사유를 요구한다.
|
||||
|
||||
### Epic: [tests] 기능 시나리오 기반 테스트 분해
|
||||
|
||||
기능 변경 시 관련 테스트와 fixture만 읽어도 회귀 조건을 이해할 수 있게 테스트 토폴로지를 재구성한다.
|
||||
|
||||
- [ ] [openai-tests] `apps/edge/internal/openai/server_test.go`를 auth/routes/models, chat handler/stream, Responses, provider tunnel, workspace/metadata, tool parsing/validation 시나리오로 분리하고 거대 `fakeRunService`를 feature-local stub 또는 함수 필드 기반 test double로 축소한다. 검증: `go test ./apps/edge/internal/openai`가 통과하고 기존 테스트 케이스가 누락되지 않는다.
|
||||
- [ ] [core-tests] `packages/go/config/config_test.go`, `apps/node/internal/node/node_test.go`, `apps/edge/internal/service/model_queue_test.go`, `apps/edge/internal/service/service_test.go`, `apps/edge/internal/bootstrap/runtime_test.go`를 config 영역과 run/cancel/command/refresh/tunnel, admission/scheduling/long-context/snapshot, runtime refresh 책임별로 분리하고 반복 setup을 명시적 helper로 정리한다. 검증: `go test ./packages/go/config ./apps/node/internal/node ./apps/edge/internal/service ./apps/edge/internal/bootstrap`가 통과한다.
|
||||
- [ ] [adapter-command-tests] `apps/node/internal/adapters`의 1,000줄 이상 adapter/CLI 테스트와 `apps/edge/cmd/edge`, `apps/control-plane/cmd/control-plane`, `apps/edge/internal/configrefresh`의 거대 command/config refresh 테스트를 실행 모드와 command 책임별로 분리한다. 검증: 대상 패키지 테스트가 통과하고 audit 결과에 사유 없는 1,000줄 초과 테스트가 남지 않는다.
|
||||
- [ ] [client-tests] `apps/client/test/widget_test.dart`를 app shell과 panel/integration 시나리오별 파일로 분리하고 공통 harness가 개별 테스트 의도를 가리지 않게 축소한다. 검증: `make client-test`가 통과한다.
|
||||
|
||||
### Epic: [modules] 동일 패키지 내 책임 중심 소스 분해
|
||||
|
||||
새 package/API를 먼저 만들지 않고 기존 package 경계를 유지한 채 파일명만으로 주요 책임을 찾을 수 있게 한다.
|
||||
|
||||
- [ ] [openai-modules] `types.go`, `stream.go`, `chat_handler.go`, `responses_handler.go`를 chat/Responses DTO, text tool parser, schema/command normalization, normalized SSE, provider tunnel/rewrite, route/policy 책임별 파일로 분리한다. 검증: 공개·내부 동작 계약을 바꾸지 않고 `go test ./apps/edge/internal/openai`가 통과한다.
|
||||
- [ ] [config-modules] `packages/go/config/config.go`를 Node/Edge/provider/adapter 타입, load/default, normalization, validation 책임별 파일로 분리하고 테스트 파일과 대응시킨다. 검증: config 예시 roundtrip과 `go test ./packages/go/config`가 통과한다.
|
||||
- [ ] [service-node-modules] Edge `run_dispatch.go`/`model_queue.go`와 Node `node.go`를 run/provider resolution/provider pool/tunnel/cancel/session, admission/release/snapshot, run/tunnel/command/refresh/sink 책임별 파일로 분리한다. 검증: `go test ./apps/edge/internal/service ./apps/node/internal/node`가 통과한다.
|
||||
- [ ] [secondary-modules] OpenAI-compatible/vLLM/Ollama/CLI adapter, Flutter runtime panel, E2E shell처럼 500~800줄 이상인 후순위 후보를 동일한 책임/변경 이유 기준으로 분해하고, 단순 LOC 충족을 위한 micro-package나 `utils` 집합은 만들지 않는다.
|
||||
|
||||
### Epic: [complexity] 장기 함수와 상태 전달 구조화
|
||||
|
||||
파일 이동만으로 남는 함수 내부 인지 복잡도를 명명된 상태와 단계별 처리 경계로 낮춘다.
|
||||
|
||||
- [ ] [stream-session] `streamChatCompletion`의 content/reasoning/tool/usage/trace 상태와 terminal event 처리를 명명된 stream session/context와 event별 단계로 분리한다. 검증: live SSE, buffered strict output, tool-call, cancel/timeout, usage 회귀 테스트가 통과한다.
|
||||
- [ ] [dispatch-context] 10개 이상 parameter를 전달하는 Chat/Responses/provider-pool 경로를 명명된 request/dispatch context로 바꾸고, queue reservation과 release의 성공/실패 경로를 한눈에 추적할 수 있게 한다.
|
||||
- [ ] [long-functions] 120줄 이상 함수의 책임 혼합 여부를 점검하고 `Classify`, adapter `Execute`, Node/transport handler와 E2E orchestration을 단계별 함수로 축소하거나 응집된 예외 사유를 남긴다.
|
||||
|
||||
### Epic: [agent-context] Agent-Ops와 구조화 인벤토리 읽기 비용 절감
|
||||
|
||||
에이전트가 실행 전에 반드시 읽는 문서는 짧고 결정적으로 유지하고, 큰 데이터는 필요한 조각만 조회할 수 있게 한다.
|
||||
|
||||
- [ ] [skill-entrypoints] `update-roadmap`, `plan`, `code-review` Skill의 trigger/input/core workflow/output/금지 사항은 진입 파일에 유지하고, 큰 출력 template과 조건부 mode reference를 직접 링크로 분리한다. plan/code-review 짝 계약과 최대 4홉 라우팅은 유지하며 단순 일관성 목적의 합병은 하지 않는다. 이 변경은 코드/테스트 분해와 분리된 전용 split task에서 수행하고 active plan/code-review loop의 파일 계약을 중간 상태로 바꾸지 않는다.
|
||||
- [ ] [inventory-query] `agent-test/dev/inventory.yaml`과 `agent-test/dev-corp/inventory.yaml`의 canonical machine-readable 역할은 유지하면서 env/model/node/provider selector로 필요한 bounded 결과만 얻는 결정적 query 경로를 제공한다.
|
||||
- [ ] [evidence-links] 활성 코드의 bare SDD section anchor를 현재 invariant 또는 contract/spec 식별자로 보강하고, 파일 분해 뒤 domain rule과 agent-spec `source_evidence`가 실제 현재 경로를 가리키도록 갱신한다.
|
||||
|
||||
### Epic: [verification] 동작 보존과 가독성 회귀 검증
|
||||
|
||||
구조 변경이 사용자 실행 계약을 바꾸지 않았고 새 기준선이 실질적인 읽기 범위 축소로 이어졌음을 검증한다.
|
||||
|
||||
- [ ] [regression] 전체 Go/Flutter 회귀와 변경 범위의 E2E/full-cycle 검증을 수행한다. 검증: `go test ./...`, `make client-test`와 testing domain rule이 요구하는 관련 smoke/full-cycle 경로가 통과한다.
|
||||
- [ ] [read-set] 대표 변경 시나리오인 OpenAI chat stream, provider-pool dispatch, Node config refresh, Flutter runtime panel, roadmap update마다 필수 규칙·주요 소스·직접 테스트 파일 목록과 LOC 합계를 재현 가능한 audit evidence로 남긴다. 각 task-local read set은 가능한 경우 1,500~2,000줄 안으로 축소하고, 초과 항목은 응집도 근거와 예외를 기록한다.
|
||||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 없음
|
||||
- 요청일: 없음
|
||||
- 완료 근거: 기능 Task가 아직 충족되지 않았다.
|
||||
- 검토 항목:
|
||||
- [ ] 생성물 예외를 제외한 P0/P1 거대 파일이 기준선 아래로 분해되거나 명시적 예외 근거를 가진다
|
||||
- [ ] 전체 Go/Flutter 회귀와 관련 E2E/full-cycle 검증이 통과한다
|
||||
- [ ] Agent-Ops 라우팅과 plan/code-review/update-roadmap 실행 의미가 보존된다
|
||||
- [ ] domain rule, agent-spec, contract pointer가 이동된 현재 코드 경로와 일치한다
|
||||
- agent-ui 상태 반영: 해당 없음
|
||||
- 리뷰 코멘트: 없음
|
||||
|
||||
## 범위 제외
|
||||
|
||||
- OpenAI-compatible/A2A API, proto wire, config schema, runtime routing 정책의 기능 변경
|
||||
- LOC를 맞추기 위한 package 남발, 타입 하나당 파일 하나 생성, 범용 `utils`/`common` 패키지 승격
|
||||
- protobuf/Dart 생성물, lockfile, PBX project, data-only fixture에 대한 동일 LOC 제한 강제
|
||||
- 기존 Git history에서 binary blob을 제거하는 history rewrite
|
||||
- archive 문서 재포맷이나 과거 완료 작업의 일반 컨텍스트 로딩
|
||||
- 리팩터링 과정에서 발견한 신규 제품 기능의 동시 구현
|
||||
|
||||
## 작업 컨텍스트
|
||||
|
||||
- 관련 경로: `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/edge/cmd/edge`, `apps/node/internal/node`, `apps/node/internal/adapters`, `apps/control-plane/cmd/control-plane`, `packages/go/config`, `apps/client`, `scripts`, `agent-ops`, `agent-test`, `agent-spec`
|
||||
- 표준선(선택): 같은 package 안에서 테스트와 파일을 먼저 기계적으로 분해하고 회귀 검증을 확보한 뒤 장기 함수와 상태 전달을 구조화한다. 파일 수보다 변경 이유의 응집도와 task-local read set을 우선한다. Agent-Ops 진입 파일 변경은 별도 split task로 마지막에 수행한다.
|
||||
- 기준선(2026-07-16): Git이 추적하는 Go/Dart/Shell/Python/Kotlin/Swift 중 `proto/gen/**`와 `apps/client/lib/gen/**`를 제외한 사람이 관리하는 코드·테스트는 242개, 파일 LOC 중앙값은 216줄, 500줄 이상은 54개, 1,000줄 이상은 22개이며 이 중 테스트가 14개, 운영 코드가 8개다. 루트 추적 ELF 3개는 합계 65,474,597byte(약 65.5MB)다.
|
||||
- SDD 재판정 조건: 리팩터링 중 외부 API/proto/config schema, runtime routing 정책, plan/code-review/update-roadmap의 실행 의미 변경이 필요해지면 해당 변경을 현재 범위에서 중단하고 roadmap/SDD gate를 재판정한다.
|
||||
- 선행 작업: 없음
|
||||
- 후속 작업: 필요하면 Epic별 split 구현 계획으로 나누되 같은 Milestone 완료 근거로 수렴한다.
|
||||
- 확인 필요: 없음
|
||||
|
|
@ -4,65 +4,68 @@
|
|||
|
||||
## 실행 순서
|
||||
|
||||
1. [OpenAI-compatible 출력 검증 필터](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)
|
||||
1. [에이전트 작업성 중심 저장소 구조 리팩터링](phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
거대 소스·테스트·Agent-Ops 문서를 책임 단위로 분해하고 저장소 가독성 기준선을 만든다.
|
||||
|
||||
2. [OpenAI-compatible 출력 검증 필터](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)
|
||||
OpenAI-compatible stream 반복 출력과 JSON contract 검증/repair 경로를 안정화한다.
|
||||
|
||||
2. [OpenAI-compatible Incomplete Tool Call Syntax Gate](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-incomplete-tool-call-syntax-gate.md)
|
||||
3. [OpenAI-compatible Incomplete Tool Call Syntax Gate](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-incomplete-tool-call-syntax-gate.md)
|
||||
terminal provider 응답의 incomplete tool-call syntax를 deterministic하게 판정한다.
|
||||
|
||||
3. [OpenAI-compatible Runtime Output Integrity Filter](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-runtime-output-integrity-filter.md)
|
||||
4. [OpenAI-compatible Runtime Output Integrity Filter](phase/knowledge-tool-optimization-extension/milestones/openai-compatible-runtime-output-integrity-filter.md)
|
||||
terminal output invariant와 공통 filter/retry pipeline을 정의한다.
|
||||
|
||||
4. [LLM 판별 기반 Missing Tool Call 재시도 Gate](phase/knowledge-tool-optimization-extension/milestones/llm-judged-missing-tool-call-retry-gate.md)
|
||||
5. [LLM 판별 기반 Missing Tool Call 재시도 Gate](phase/knowledge-tool-optimization-extension/milestones/llm-judged-missing-tool-call-retry-gate.md)
|
||||
tool 사용 의도 누락 케이스를 LLM judge와 buffered retry 후보로 검토한다.
|
||||
|
||||
5. [Tool Call 판정 모델 Gate 리뷰](phase/knowledge-tool-optimization-extension/milestones/tool-call-validator-model-gate-review.md)
|
||||
6. [Tool Call 판정 모델 Gate 리뷰](phase/knowledge-tool-optimization-extension/milestones/tool-call-validator-model-gate-review.md)
|
||||
schema만으로 어려운 tool-call 후보에 validator 모델을 쓸지 검토한다.
|
||||
|
||||
6. [OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화](phase/routing-policy-model-orchestration/milestones/openai-compatible-hybrid-routing-context-optimization.md)
|
||||
7. [OpenAI-compatible 하이브리드 라우팅과 컨텍스트 최적화](phase/routing-policy-model-orchestration/milestones/openai-compatible-hybrid-routing-context-optimization.md)
|
||||
skill route, local triage, score policy, context optimizer를 결합한 hybrid route 방향을 구체화한다.
|
||||
|
||||
7. [Pi CLI Provider Integration](phase/automation-runtime-bridge/milestones/pi-cli-provider-integration.md)
|
||||
8. [Pi CLI Provider Integration](phase/automation-runtime-bridge/milestones/pi-cli-provider-integration.md)
|
||||
Pi를 Node CLI provider 실행 후보에 추가하고 OpenAI-compatible route smoke로 안정화한다.
|
||||
|
||||
8. [CLI Agent Group Grade Routing](phase/automation-runtime-bridge/milestones/cli-agent-group-grade-routing.md)
|
||||
9. [CLI Agent Group Grade Routing](phase/automation-runtime-bridge/milestones/cli-agent-group-grade-routing.md)
|
||||
lane/grade 파일명과 `metadata.agent_group.task_file` 기반 CLI agent group 라우팅 계약을 정리한다.
|
||||
|
||||
9. [CLI Agent 사용량 알림과 자동 이어받기 MVP](phase/automation-runtime-bridge/milestones/cli-agent-usage-notification-continuation.md)
|
||||
10. [CLI Agent 사용량 알림과 자동 이어받기 MVP](phase/automation-runtime-bridge/milestones/cli-agent-usage-notification-continuation.md)
|
||||
CLI Agent limit 감지, 사용자 알림, 작업 자동 이어받기 경계를 스케치한다.
|
||||
|
||||
10. [단계 호출과 검증 최적화 MVP](phase/knowledge-tool-optimization-extension/milestones/knowledge-tool-validation-optimization.md)
|
||||
11. [단계 호출과 검증 최적화 MVP](phase/knowledge-tool-optimization-extension/milestones/knowledge-tool-validation-optimization.md)
|
||||
planner/generator/verifier 단계 호출과 runtime schema 검증 실행 모드를 스케치한다.
|
||||
|
||||
11. [원격 코딩/유지보수 작업 환경](phase/automation-runtime-bridge/milestones/remote-workspace-operations-environment.md)
|
||||
12. [원격 코딩/유지보수 작업 환경](phase/automation-runtime-bridge/milestones/remote-workspace-operations-environment.md)
|
||||
workspace-bound execution 기반 원격 코딩/유지보수 운영 경계를 스케치한다.
|
||||
|
||||
12. [Personal Local Edge 패키징과 배포 모드 프로파일](phase/personal-edge-packaging-deployment/milestones/personal-local-edge-deployment-profiles.md)
|
||||
13. [Personal Local Edge 패키징과 배포 모드 프로파일](phase/personal-edge-packaging-deployment/milestones/personal-local-edge-deployment-profiles.md)
|
||||
personal/server/fleet 배포 모드와 capability gate 경계를 스케치한다.
|
||||
|
||||
13. [요청 실행 로그와 Usage Ledger 기반](phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md)
|
||||
14. [요청 실행 로그와 Usage Ledger 기반](phase/operational-observability-provider-management/milestones/request-execution-log-usage-ledger-foundation.md)
|
||||
요청별 provider/model 선택, timing, token, status/error를 구조화된 ledger로 남기는 기반을 스케치한다.
|
||||
|
||||
14. [Update Plane 안정 프로토콜](phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md)
|
||||
15. [Update Plane 안정 프로토콜](phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md)
|
||||
hello/status, manifest, command, event, recovery 최소 계약을 스케치한다.
|
||||
|
||||
15. [Host-local Manager 기반 자체 업데이트](phase/update-plane-self-update-foundation/milestones/host-local-manager-self-update.md)
|
||||
16. [Host-local Manager 기반 자체 업데이트](phase/update-plane-self-update-foundation/milestones/host-local-manager-self-update.md)
|
||||
manager/updater의 release staging, 검증, restart, rollback 실행 모델을 정리한다.
|
||||
|
||||
16. [Edge/Node 롤아웃과 복구 정책](phase/update-plane-self-update-foundation/milestones/edge-node-rollout-recovery-policy.md)
|
||||
17. [Edge/Node 롤아웃과 복구 정책](phase/update-plane-self-update-foundation/milestones/edge-node-rollout-recovery-policy.md)
|
||||
Edge/Node rolling update, 실패/재연결/rollback 보고 정책을 스케치한다.
|
||||
|
||||
17. [Provider Runtime 설정과 모델 획득 오케스트레이션](phase/operational-observability-provider-management/milestones/provider-runtime-model-acquisition-orchestration.md)
|
||||
18. [Provider Runtime 설정과 모델 획득 오케스트레이션](phase/operational-observability-provider-management/milestones/provider-runtime-model-acquisition-orchestration.md)
|
||||
provider runtime launch/profile, model download/cache/verification 경계를 스케치한다.
|
||||
|
||||
18. [Provider-Device-Model Qualification 리포트와 Lifecycle 관리](phase/operational-observability-provider-management/milestones/provider-device-model-qualification-report.md)
|
||||
19. [Provider-Device-Model Qualification 리포트와 Lifecycle 관리](phase/operational-observability-provider-management/milestones/provider-device-model-qualification-report.md)
|
||||
provider/device/model별 compatibility, performance, quality, lifecycle 리포트 경계를 정리한다.
|
||||
|
||||
19. [장기 기억과 RAG 업데이트 사이클 (2차)](phase/knowledge-tool-optimization-extension/milestones/long-term-memory-rag-second-wave.md)
|
||||
20. [장기 기억과 RAG 업데이트 사이클 (2차)](phase/knowledge-tool-optimization-extension/milestones/long-term-memory-rag-second-wave.md)
|
||||
repo 장기 기억, RAG 저장소, update cycle, MCP 기반 context 절약 후보를 스케치한다.
|
||||
|
||||
20. [Advisor와 Context Hook 최적화 (2차)](phase/knowledge-tool-optimization-extension/milestones/advisor-context-hook-second-wave.md)
|
||||
21. [Advisor와 Context Hook 최적화 (2차)](phase/knowledge-tool-optimization-extension/milestones/advisor-context-hook-second-wave.md)
|
||||
advisor 역할과 local LLM hook 기반 context compression 후보를 스케치한다.
|
||||
|
||||
21. [oto 자동화 스케줄러와 CI-CD 연동 (2차)](phase/automation-runtime-bridge/milestones/oto-automation-scheduler-second-wave.md)
|
||||
22. [oto 자동화 스케줄러와 CI-CD 연동 (2차)](phase/automation-runtime-bridge/milestones/oto-automation-scheduler-second-wave.md)
|
||||
oto 기반 자동화, scheduler, CI-CD 연동 후보를 스케치한다.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/01_openai_auth_chat plan=1 tag=REVIEW_TEST -->
|
||||
|
||||
# Code Review Reference - REVIEW_TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/01_openai_auth_chat, plan=1, tag=REVIEW_TEST
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- Previous task: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat`
|
||||
- Archived plan: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/plan_local_G07_0.log`
|
||||
- Archived review: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/code_review_local_G07_0.log`
|
||||
- Verdict: FAIL
|
||||
- Findings: Required 1, Suggested 0, Nit 0. 공용 `SubmitRun`/`OllamaAPI` 구현과 `bufferedRunEvents`가 `openai_auth_routes_models_test.go`에 남아 계획의 support 책임 경계를 위반했다.
|
||||
- Affected files: `apps/edge/internal/openai/server_test_support_test.go`, `apps/edge/internal/openai/openai_auth_routes_models_test.go`
|
||||
- Verification evidence: `gofmt -l apps/edge/internal/openai/*_test.go` PASS, Test 목록 226개, `GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai` PASS(6.730s), `GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/...` PASS.
|
||||
- Roadmap carryover: 원 계획에 `Roadmap Targets`가 없어 Milestone Task completion은 주장하지 않는다. 선택 Milestone의 SDD는 불필요다.
|
||||
- Allowed archive reread: 위 `plan_local_G07_0.log`, `code_review_local_G07_0.log`만 필요할 때 좁게 읽는다.
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G04.md` → `code_review_local_G04_N.log`, `PLAN-local-G04.md` → `plan_local_G04_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/01_openai_auth_chat/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_TEST-1] 공용 test double과 fixture 위치 정리 | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [REVIEW_TEST-1] 공용 `SubmitRun`/`OllamaAPI`/`bufferedRunEvents`를 support 파일로 옮기고 import를 정리하며 Test 목록 226개와 fresh package test 통과를 보존한다.
|
||||
- [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_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [x] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
계획대로 구현했다. openai_auth_routes_models_test.go에서 SubmitRun, OllamaAPI, bufferedRunEvents 세 선언을 server_test_support_test.go로 이동하고, 불필요해진 context, fmt, iop import를 제거했다. server_test_support_test.go에 fmt import를 추가했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
없음. 기계적 선언 이동이므로 설계 결정 사항이 없다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- `fakeRunService`의 interface method와 공용 event helper가 support 파일에 함께 있는가.
|
||||
- auth/routes/models 파일이 해당 시나리오 테스트와 feature-local assertion만 보유하는가.
|
||||
- 선언 본문, Test 226개, package 동작이 이동 전과 동일한가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### REVIEW_TEST-1 중간 검증
|
||||
```bash
|
||||
$ gofmt -w apps/edge/internal/openai/server_test_support_test.go apps/edge/internal/openai/openai_auth_routes_models_test.go
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l
|
||||
226
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
ok iop/apps/edge/internal/openai 6.718s
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l
|
||||
226
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- implementation deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: `complete.log`를 작성하고 task directory를 archive한 뒤 milestone 완료 이벤트 메타데이터를 보고한다.
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/01_openai_auth_chat plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/01_openai_auth_chat, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/01_openai_auth_chat/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] auth/chat 시나리오 파일과 test double 분리 | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [TEST-1] auth/routes/models와 chat handler/stream 테스트를 책임별 파일로 옮기고 Test 목록 226개 및 package test 통과를 보존한다.
|
||||
- [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_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
### 수정 사항
|
||||
|
||||
Plan의 의도를 정확히 반영하기 위해 초기 구현에서 라인 경계 조정을 진행했습니다:
|
||||
|
||||
**초기 구현 (문제)**:
|
||||
- `openai_auth_routes_models_test.go`에 라인 718-970의 chat handler 테스트가 포함됨
|
||||
- `server_test.go`에 라인 3079-3193의 models/Ollama 테스트가 남아있음
|
||||
|
||||
**수정 내용**:
|
||||
- `openai_auth_routes_models_test.go`: 라인 618-717 (auth/routes/models) + 라인 3079-3193 (models/Ollama)
|
||||
- `chat_handler_test.go`: 라인 718-1774 (non-stream chat handler)
|
||||
- `server_test.go`: 라인 3194-8135 (Responses, workspace, provider pool - 후속 sibling 범위)
|
||||
|
||||
### 검증 명령
|
||||
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
ok iop/apps/edge/internal/openai 6.716s
|
||||
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l
|
||||
226
|
||||
```
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- **파일 분할 기준**: 원본 파일의 라인 번호만 사용하지 않고, 각 테스트의 이름과 책임을 기준으로 분할하여 의미 있는 단위로 그룹화했다.
|
||||
- **지원 코드 분리(구현 상태)**: 공용 타입과 provider tunnel/cancel helper는 `server_test_support_test.go`로 이동했다. 다만 `SubmitRun`, `OllamaAPI`, `bufferedRunEvents`는 `openai_auth_routes_models_test.go`에 남아 있어 리뷰 후속 보완이 필요하다.
|
||||
- **인터페이스 구현 고려**: `runService` 인터페이스를 구현하는 `fakeRunService`의 메서드는 공용 support와 함께 두는 것이 목표였으나 이번 구현에서는 일부가 feature 파일에 남았다.
|
||||
- **import 최소화**: 각 테스트 파일에 필요한 import만 포함하여 컴파일 에러를 방지하고 가독성을 높였다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 이동 전후 top-level Test 226개와 이름이 보존되었는가.
|
||||
- 공유 support가 실제 공통 fixture만 남기고 feature별 assertion을 숨기지 않는가.
|
||||
- auth/chat 파일 경계가 01 plan 범위를 넘지 않는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```bash
|
||||
$ gofmt -w apps/edge/internal/openai/*_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
```
|
||||
- gofmt 실행 후 리포팅: 모든 파일 포맷팅 완료
|
||||
- 테스트 목록 검증: 226개 테스트 보존 확인
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
ok iop/apps/edge/internal/openai 6.733s
|
||||
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l
|
||||
226
|
||||
```
|
||||
- 전체 테스트 통과: 226개 테스트 중 0개 실패
|
||||
- 테스트 수 보존: 226개 확인 (원본 226개와 일치)
|
||||
- 빌드 시간: 6.733초
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Fail
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Fail
|
||||
- implementation deviation: Fail
|
||||
- verification trust: Pass
|
||||
- 발견된 문제:
|
||||
- Required — `apps/edge/internal/openai/openai_auth_routes_models_test.go:59`, `apps/edge/internal/openai/openai_auth_routes_models_test.go:100`, `apps/edge/internal/openai/openai_auth_routes_models_test.go:108`: 공용 `fakeRunService`의 `SubmitRun`/`OllamaAPI` 구현과 여러 chat/stream/provider 시나리오가 사용하는 `bufferedRunEvents`가 auth/routes/models 파일에 남아 있다. 이 때문에 chat 계열 테스트의 직접 read set이 무관한 auth 파일까지 확장되고, 계획의 “공유 test double과 fixture는 `server_test_support_test.go`에 둔다”는 책임 경계를 충족하지 못한다. 세 선언을 `server_test_support_test.go`로 옮기고 두 파일의 import를 최소화한 뒤 226개 Test 목록과 fresh package test를 다시 검증한다.
|
||||
- 다음 단계: 동일 task path에 이 Required 항목만 다루는 후속 plan/review를 생성한다.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Complete - m-agent-readable-repository-refactor/01_openai_auth_chat
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-07-16
|
||||
|
||||
## 요약
|
||||
|
||||
OpenAI-compatible 테스트를 책임별 파일로 분리하고 공용 test double/fixture를 support 파일로 모으는 작업을 2개 리뷰 루프 끝에 PASS로 완료했다.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_local_G07_0.log` | `code_review_local_G07_0.log` | FAIL | 테스트 분해와 226개 Test 보존은 확인됐으나 `SubmitRun`, `OllamaAPI`, `bufferedRunEvents`가 auth/routes/models 파일에 남아 후속 정리가 필요했다. |
|
||||
| `plan_local_G04_1.log` | `code_review_local_G04_1.log` | PASS | 세 공용 선언을 `server_test_support_test.go`로 이동하고 import와 책임 경계를 정리했으며 fresh package test와 Test 개수 검증이 통과했다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- auth/routes/models와 chat handler/stream 테스트를 책임별 파일로 분리하고 기존 top-level Test 226개를 보존했다.
|
||||
- `fakeRunService.SubmitRun`, `fakeRunService.OllamaAPI`, `bufferedRunEvents`를 `server_test_support_test.go`로 이동해 공용 test support의 직접 read set을 정리했다.
|
||||
- 선언 본문과 OpenAI-compatible API/runtime 동작은 변경하지 않았다. Spec update not needed: 테스트 선언 위치만 바꾼 동작 보존형 리팩터링이다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `gofmt -l apps/edge/internal/openai/server_test_support_test.go apps/edge/internal/openai/openai_auth_routes_models_test.go` - PASS; 출력 없음.
|
||||
- `GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai` - PASS; `ok iop/apps/edge/internal/openai 6.723s`.
|
||||
- `GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l` - PASS; `226`.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/01_openai_auth_chat plan=1 tag=REVIEW_TEST -->
|
||||
|
||||
# OpenAI 테스트 공용 support 책임 경계 보완
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 검증을 실행하고 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 `구현 잠금 > 결정 필요` 항목이 실제 구현을 차단할 때만 review stub의 `사용자 리뷰 요청`에 정확한 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나 `USER_REVIEW.md`/`complete.log`를 만들거나 active 파일을 archive하지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
첫 리뷰에서 테스트 본문, 226개 Test 이름, package 동작은 보존됐지만 공용 test double 구현 일부가 auth/routes/models 파일에 남은 것이 확인됐다. 이 후속 작업은 공용 fixture의 실제 위치만 바로잡아 chat/stream/provider 테스트의 직접 read set에서 무관한 auth 파일 의존을 제거한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 섹션 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 따르며, 구현 중 직접 사용자 prompt는 금지한다. 요청의 타당성 검증과 실제 `USER_REVIEW.md` 작성은 code-review가 소유한다.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- Previous task: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat`
|
||||
- Archived plan: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/plan_local_G07_0.log`
|
||||
- Archived review: `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/code_review_local_G07_0.log`
|
||||
- Verdict: FAIL
|
||||
- Findings: Required 1, Suggested 0, Nit 0. 공용 `SubmitRun`/`OllamaAPI` 구현과 `bufferedRunEvents`가 `openai_auth_routes_models_test.go`에 남아 계획의 support 책임 경계를 위반했다.
|
||||
- Affected files: `apps/edge/internal/openai/server_test_support_test.go`, `apps/edge/internal/openai/openai_auth_routes_models_test.go`
|
||||
- Verification evidence: `gofmt -l apps/edge/internal/openai/*_test.go` PASS, Test 목록 226개, `GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai` PASS(6.730s), `GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/...` PASS.
|
||||
- Roadmap carryover: 원 계획에 `Roadmap Targets`가 없어 Milestone Task completion은 주장하지 않는다. 선택 Milestone의 SDD는 불필요다.
|
||||
- Allowed archive reread: 위 `plan_local_G07_0.log`, `code_review_local_G07_0.log`만 필요할 때 좁게 읽는다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/plan_local_G07_0.log`
|
||||
- `agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/code_review_local_G07_0.log`
|
||||
- `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- `agent-ops/rules/project/domain/edge/rules.md`
|
||||
- `agent-ops/rules/project/domain/testing/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/edge-smoke.md`
|
||||
- `agent-test/local/testing-smoke.md`
|
||||
- `agent-spec/input/openai-compatible-surface.md`
|
||||
- `agent-contract/outer/openai-compatible-api.md`
|
||||
- `apps/edge/internal/openai/server.go`
|
||||
- `apps/edge/internal/openai/server_test_support_test.go`
|
||||
- `apps/edge/internal/openai/openai_auth_routes_models_test.go`
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 `SDD: 불필요`다. 외부 API/proto/config schema나 runtime 책임 경계를 바꾸지 않는 동일 package 테스트 재배치라는 기록 사유를 그대로 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
`test_env=local`. `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/testing-smoke.md`를 읽었다. 테스트 선언 위치와 import만 바꾸므로 변경 package의 fresh test와 Test 목록 226개 보존을 필수 검증으로 적용한다. production 실행 경로를 바꾸지 않아 외부 provider, smoke, full-cycle 검증은 생략하며 남은 runtime 위험은 없다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 226개 Test의 본문과 이름을 바꾸지 않고 선언 위치만 이동하므로 신규 테스트는 필요하지 않다. package compile과 fresh test가 interface 구현 및 import 정합성을 검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
rename/remove는 없다. `bufferedRunEvents` 참조는 `chat_handler_test.go`, `chat_stream_reasoning_test.go`, `server_test.go`, `usage_metrics_test.go`에 있고, `fakeRunService.SubmitRun`/`OllamaAPI`는 `server.go`의 `runService` 구현을 구성한다. 선언만 같은 package의 support 파일로 이동한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
기존 split subtask `01_openai_auth_chat` 안의 단일 후속 계획으로 유지한다. 두 테스트 파일 사이의 세 선언 이동과 import 정리만 포함해 독립 API·도메인·검증 경계가 없고, 더 나누면 동일 package compile 상태를 불필요하게 중간 분할한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
테스트 함수 본문, fake 동작, production 소스, `server_test.go`, 다른 sibling task, roadmap/spec/contract는 수정하지 않는다. 첫 리뷰의 유일한 Required finding만 해결한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
`local-G04`: 두 테스트 파일의 기계적 선언 이동이며 Test 목록과 fresh package test로 결정적으로 검증할 수 있다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [REVIEW_TEST-1] 공용 `SubmitRun`/`OllamaAPI`/`bufferedRunEvents`를 support 파일로 옮기고 import를 정리하며 Test 목록 226개와 fresh package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REVIEW_TEST-1] 공용 test double과 fixture 위치 정리
|
||||
|
||||
- 문제: `apps/edge/internal/openai/openai_auth_routes_models_test.go:59`, `:100`, `:108`에 package 전반이 의존하는 `fakeRunService` interface method와 `bufferedRunEvents`가 남아 있어 chat/stream/provider 테스트가 auth 파일을 간접적으로 읽어야 한다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
openai_auth_routes_models_test.go SubmitRun, OllamaAPI, bufferedRunEvents + auth/routes/models/Ollama tests
|
||||
server_test_support_test.go fakeRunService type + 나머지 interface method/helper
|
||||
|
||||
After
|
||||
openai_auth_routes_models_test.go auth/routes/models/Ollama tests only
|
||||
server_test_support_test.go fakeRunService type + 모든 interface method + shared bufferedRunEvents
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] `apps/edge/internal/openai/server_test_support_test.go`: 세 선언을 본문 변경 없이 `fakeRunService` support와 함께 이동하고 `fmt` import를 추가한다.
|
||||
- [ ] `apps/edge/internal/openai/openai_auth_routes_models_test.go`: 세 선언을 제거하고 불필요해진 `context`, `fmt`, `iop` import를 제거하되 auth/routes/models/Ollama 테스트 본문은 바꾸지 않는다.
|
||||
- 테스트 작성: 추가하지 않는다. 동작 변경 없는 선언 이동이며 기존 226개 Test와 package compile이 누락·중복·interface/import 오류를 검출한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/internal/openai/server_test_support_test.go apps/edge/internal/openai/openai_auth_routes_models_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: gofmt 성공, top-level Test 226개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| `apps/edge/internal/openai/server_test_support_test.go` | REVIEW_TEST-1 |
|
||||
| `apps/edge/internal/openai/openai_auth_routes_models_test.go` | REVIEW_TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: fresh package test PASS와 Test 226개 보존. Go test cache 결과는 허용하지 않는다.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/01_openai_auth_chat plan=0 tag=TEST -->
|
||||
|
||||
# OpenAI auth/chat 테스트 시나리오 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 검증을 실행하고 CODE_REVIEW 파일의 구현 에이전트 소유 섹션에 실제 메모와 출력을 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 CODE_REVIEW의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하거나 USER_REVIEW.md/complete.log를 만들지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
apps/edge/internal/openai/server_test.go는 8,135줄에 auth, models, chat, stream, tool 합성 테스트와 범용 fake가 함께 있다. 첫 단계에서 앞부분의 공통 support와 chat 계열을 기능 파일로 이동해 이후 Responses/provider 분해가 충돌 없이 이어지게 한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 기록한다. 섹션 형식은 agent-ops/skills/common/_templates/implementation-user-review-request-section.md를 따른다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-test/local/profiles/testing-smoke.md
|
||||
- agent-spec/input/openai-compatible-surface.md
|
||||
- apps/edge/internal/openai/server_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. agent-test/local/rules.md와 edge-smoke/testing-smoke profile을 읽었다. 현재 checkout에서 GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai가 통과했다. 구조 리팩터링이므로 fresh package test와 Test 목록 수 226 보존을 계약으로 사용하며 cache 결과는 허용하지 않는다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 226개 top-level Test가 현재 계약을 덮고 있으며 이 단계는 이름·assertion을 그대로 이동한다. 신규 회귀 테스트는 필요하지 않다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 그대로 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
공유 task group은 agent-task/m-agent-readable-repository-refactor다. 01_openai_auth_chat은 독립 시작점이고, 02+01_openai_responses_workspace와 03+02_openai_provider_tools가 순서대로 같은 원본 파일의 나머지를 소비한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
server_test.go의 1~3,193줄과 그 구간에서 직접 사용하는 test support만 이동한다. Responses, workspace, provider tunnel/pool, production 소스는 후속 sibling 범위다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 한 package 안의 동작 보존 이동이지만 3,000줄 이상과 공유 fake/import 정리가 포함되어 높은 국소 문맥이 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] auth/routes/models와 chat handler/stream 테스트를 책임별 파일로 옮기고 Test 목록 226개 및 package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] auth/chat 시나리오 파일과 test double 분리
|
||||
|
||||
- 문제: apps/edge/internal/openai/server_test.go:618의 auth 테스트부터 3,193줄의 models/Ollama/chat 테스트까지 하나의 파일과 1~617줄의 범용 support에 결합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/edge/internal/openai/server_test.go:618 func TestRoutesRequireBearerTokenWhenConfigured(...)
|
||||
apps/edge/internal/openai/server_test.go:718 func TestChatCompletionsDispatchesConfiguredOllamaTarget(...)
|
||||
apps/edge/internal/openai/server_test.go:2271 func TestChatCompletionsStreamsSSE(...)
|
||||
|
||||
After
|
||||
server_test_support_test.go 공통 최소 fixture
|
||||
openai_auth_routes_models_test.go auth/routes/models/Ollama
|
||||
chat_handler_test.go 일반 chat handler
|
||||
chat_tool_synthesis_test.go tool 합성/검증 전반부
|
||||
chat_stream_reasoning_test.go stream/reasoning/strict output
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/edge/internal/openai/server_test.go: 해당 테스트/support를 제거하되 후속 구간은 유지한다.
|
||||
- [ ] apps/edge/internal/openai/server_test_support_test.go: 공유가 필요한 최소 함수 필드 double과 fixture만 둔다.
|
||||
- [ ] apps/edge/internal/openai/openai_auth_routes_models_test.go: auth/routes/models/Ollama 테스트를 옮긴다.
|
||||
- [ ] apps/edge/internal/openai/chat_handler_test.go: non-stream chat 테스트를 옮긴다.
|
||||
- [ ] apps/edge/internal/openai/chat_tool_synthesis_test.go: tool 합성 테스트를 옮긴다.
|
||||
- [ ] apps/edge/internal/openai/chat_stream_reasoning_test.go: stream/reasoning/strict output 테스트를 옮긴다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. internal refactor이며 기존 테스트 이름, table case, assertion, fixture 값을 그대로 보존하고 aggregate 목록 수로 누락을 검출한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/internal/openai/*_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: gofmt 성공, top-level Test 226개.
|
||||
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/edge/internal/openai/server_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/server_test_support_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/openai_auth_routes_models_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/chat_handler_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/chat_tool_synthesis_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/chat_stream_reasoning_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: fresh package test PASS와 Test 226개 보존.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/02+01_openai_responses_workspace plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/02+01_openai_responses_workspace, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Responses/workspace/cancel 시나리오 파일 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Responses, metadata/workspace, cancel/route 테스트를 책임별 파일로 옮기고 Test 목록 226개 및 package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 01 predecessor complete.log가 구현 전에 존재했는가.
|
||||
- Responses/workspace/cancel Test 이름과 assertion이 보존되었는가.
|
||||
- provider 후반부를 선행 이동하거나 삭제하지 않았는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/edge/internal/openai/*_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/02+01_openai_responses_workspace plan=0 tag=TEST -->
|
||||
|
||||
# OpenAI Responses/workspace 테스트 시나리오 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 검증을 실행하고 CODE_REVIEW 파일의 구현 에이전트 소유 섹션에 실제 메모와 출력을 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 CODE_REVIEW의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하거나 USER_REVIEW.md/complete.log를 만들지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
첫 OpenAI 분해 뒤 server_test.go에는 Responses와 workspace/cancel/route 시나리오가 provider 계열과 함께 남는다. 두 번째 단계는 3,194~4,479줄을 분리해 Responses 계약을 작은 read set으로 만든다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 기록한다. 섹션 형식은 agent-ops/skills/common/_templates/implementation-user-review-request-section.md를 따른다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-test/local/profiles/testing-smoke.md
|
||||
- agent-spec/input/openai-compatible-surface.md
|
||||
- apps/edge/internal/openai/server_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. agent-test/local/rules.md와 edge-smoke/testing-smoke profile을 읽었다. 현재 checkout에서 GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai가 통과했다. 구조 리팩터링이므로 fresh package test와 Test 목록 수 226 보존을 계약으로 사용하며 cache 결과는 허용하지 않는다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 Responses/workspace/cancel 테스트를 그대로 옮기며 package 전체 Test 226개를 보존한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 그대로 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
공유 task group은 agent-task/m-agent-readable-repository-refactor다. 이 디렉터리의 +01은 01_openai_auth_chat/complete.log를 요구한다. 현재 predecessor complete.log는 없으므로 구현 시작 전 runtime이 이를 충족해야 한다. 03+02_openai_provider_tools가 이 작업 뒤 원본을 최종 제거한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
server_test.go의 기존 3,194~4,479줄에 해당하는 Responses, metadata/workspace, cancellation, route catalog만 이동한다. provider pool/tunnel/tool 후반부와 production 소스는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 같은 대형 파일의 두 번째 기계적 분해이며 protocol별 fixture 결합을 유지해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Responses, metadata/workspace, cancel/route 테스트를 책임별 파일로 옮기고 Test 목록 226개 및 package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Responses/workspace/cancel 시나리오 파일 분리
|
||||
|
||||
- 문제: apps/edge/internal/openai/server_test.go:3194~4479에 Responses shape, metadata/workspace, cancel, route catalog가 연속 배치되어 feature-local 탐색이 어렵다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/edge/internal/openai/server_test.go:3194 func TestResponsesDispatchesNonStreamingRequest(...)
|
||||
apps/edge/internal/openai/server_test.go:3342 func TestChatCompletionsMetadataContractAndWorkspace(...)
|
||||
apps/edge/internal/openai/server_test.go:3817 func TestCollectRunResultTimesOut(...)
|
||||
|
||||
After
|
||||
responses_handler_test.go Responses shape/strict output
|
||||
workspace_metadata_test.go chat/Responses metadata와 workspace
|
||||
cancellation_routes_test.go cancel/timeout와 route catalog
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/edge/internal/openai/server_test.go: 해당 구간만 제거하고 provider 후반부를 유지한다.
|
||||
- [ ] apps/edge/internal/openai/responses_handler_test.go: Responses handler 시나리오를 옮긴다.
|
||||
- [ ] apps/edge/internal/openai/workspace_metadata_test.go: metadata/workspace 시나리오를 옮긴다.
|
||||
- [ ] apps/edge/internal/openai/cancellation_routes_test.go: cancel/timeout/route 시나리오를 옮긴다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. 기존 Test 이름과 assertion을 그대로 유지하고 package 목록 수로 누락을 검출한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/internal/openai/*_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: gofmt 성공, top-level Test 226개.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. agent-task/m-agent-readable-repository-refactor/01_openai_auth_chat/complete.log가 생성된 뒤 시작한다.
|
||||
2. 이 plan은 디렉터리 이름에 없는 추가 predecessor를 요구하지 않는다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/edge/internal/openai/server_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/responses_handler_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/workspace_metadata_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/cancellation_routes_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: fresh package test PASS와 Test 226개 보존.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/03+02_openai_provider_tools plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/03+02_openai_provider_tools, plan=0, tag=TEST
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `openai-tests`: OpenAI 서버 테스트를 기능 시나리오별 파일과 feature-local double로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G08.md` → `code_review_local_G08_N.log`, `PLAN-local-G08.md` → `plan_local_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/03+02_openai_provider_tools/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] provider/tool 시나리오 분리와 server_test.go 제거 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] provider tunnel/pool, policy, tool validation, observability 테스트를 분리하고 원본 제거, Test 226개, 1,000줄 초과 없음, package test 통과를 함께 검증한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 02 predecessor complete.log가 구현 전에 존재했는가.
|
||||
- server_test.go가 제거되고 Test 226개가 보존되었는가.
|
||||
- 모든 OpenAI test 파일이 1,000줄 이하이며 fake가 feature-local인가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/edge/internal/openai/*_test.go
|
||||
$ test ! -e apps/edge/internal/openai/server_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
$ python3 -c "from pathlib import Path; p=Path('apps/edge/internal/openai'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/03+02_openai_provider_tools plan=0 tag=TEST -->
|
||||
|
||||
# OpenAI provider/tool 테스트 최종 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 검증을 실행하고 CODE_REVIEW 파일의 구현 에이전트 소유 섹션에 실제 메모와 출력을 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 CODE_REVIEW의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하거나 USER_REVIEW.md/complete.log를 만들지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
앞선 두 단계가 auth/chat과 Responses/workspace를 제거하면 server_test.go에는 provider tunnel/pool, policy, tool validation, observability 시나리오가 남는다. 마지막 단계에서 이를 책임별 파일로 이동하고 원본 거대 파일을 제거해 openai-tests를 완료한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 기록한다. 섹션 형식은 agent-ops/skills/common/_templates/implementation-user-review-request-section.md를 따른다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `openai-tests`: OpenAI 서버 테스트를 기능 시나리오별 파일과 feature-local double로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-test/local/profiles/testing-smoke.md
|
||||
- agent-spec/input/openai-compatible-surface.md
|
||||
- apps/edge/internal/openai/server_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. agent-test/local/rules.md와 edge-smoke/testing-smoke profile을 읽었다. 현재 checkout에서 GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai가 통과했다. 구조 리팩터링이므로 fresh package test와 Test 목록 수 226 보존을 계약으로 사용하며 cache 결과는 허용하지 않는다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 provider/tool 테스트 전부를 이동하고 top-level Test 226개, 파일 크기, fresh package test로 누락과 컴파일 회귀를 검출한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 그대로 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
공유 task group은 agent-task/m-agent-readable-repository-refactor다. +02는 02+01_openai_responses_workspace/complete.log를 요구하며 현재 미충족이다. 이 최종 sibling만 Roadmap Targets의 openai-tests를 check-on-pass 한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
server_test.go의 남은 provider/tool/observability 테스트와 직접 support만 다룬다. production OpenAI 모듈 분해는 modules Epic 범위이며 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G08: 3,600줄 이상 후반부, provider path별 fixture, 최종 원본 제거와 roadmap completion 검증을 함께 다룬다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] provider tunnel/pool, policy, tool validation, observability 테스트를 분리하고 원본 제거, Test 226개, 1,000줄 초과 없음, package test 통과를 함께 검증한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] provider/tool 시나리오 분리와 server_test.go 제거
|
||||
|
||||
- 문제: apps/edge/internal/openai/server_test.go:4480~8135에 provider dispatch/tunnel/policy, tool validation, logs/selection이 한 파일에 남아 있고 거대 fake 의존이 지속된다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/edge/internal/openai/server_test.go:4480 func TestHandleModelsProviderPoolCatalog(...)
|
||||
apps/edge/internal/openai/server_test.go:6411 func TestChatCompletionsRetriesUnknownTextToolCallBeforeResponse(...)
|
||||
apps/edge/internal/openai/server_test.go:7701 func TestProviderPoolDispatchLogObservationFields(...)
|
||||
|
||||
After
|
||||
provider_dispatch_test.go pool dispatch
|
||||
provider_tunnel_test.go chat/Responses tunnel
|
||||
provider_policy_test.go generation/thinking policy
|
||||
provider_tool_validation_test.go retry/schema validation
|
||||
provider_observability_test.go metadata/log fields
|
||||
provider_selection_test.go mixed pool selection
|
||||
server_test.go removed
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/edge/internal/openai/server_test.go: 남은 테스트를 이동한 뒤 삭제한다.
|
||||
- [ ] apps/edge/internal/openai/provider_dispatch_test.go: pool dispatch 시나리오를 둔다.
|
||||
- [ ] apps/edge/internal/openai/provider_tunnel_test.go: provider tunnel 시나리오를 둔다.
|
||||
- [ ] apps/edge/internal/openai/provider_policy_test.go: generation/thinking policy를 둔다.
|
||||
- [ ] apps/edge/internal/openai/provider_tool_validation_test.go: tool retry/schema 검증을 둔다.
|
||||
- [ ] apps/edge/internal/openai/provider_observability_test.go: metadata/logging을 둔다.
|
||||
- [ ] apps/edge/internal/openai/provider_selection_test.go: mixed provider selection을 둔다.
|
||||
- [ ] apps/edge/internal/openai/server_test_support_test.go: 범용 fake를 실제 공통 함수 필드만 남기도록 축소한다.
|
||||
- 테스트 작성: 추가 동작 테스트는 작성하지 않는다. 모든 기존 Test 이름/assertion을 보존하고 목록 수와 fresh test, LOC gate를 적용한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/internal/openai/*_test.go
|
||||
test ! -e apps/edge/internal/openai/server_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 원본 삭제, gofmt 성공, Test 226개.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. agent-task/m-agent-readable-repository-refactor/02+01_openai_responses_workspace/complete.log가 생성된 뒤 시작한다.
|
||||
2. 디렉터리 이름의 predecessor 02 외 추가 의존성은 없다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/edge/internal/openai/server_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/server_test_support_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_dispatch_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_tunnel_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_policy_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_tool_validation_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_observability_test.go | TEST-1 |
|
||||
| apps/edge/internal/openai/provider_selection_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/internal/openai
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/openai | sed '/^ok[[:space:]]/d' | wc -l)" -eq 226 ]
|
||||
python3 -c "from pathlib import Path; p=Path('apps/edge/internal/openai'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: fresh package test PASS, Test 226개, 모든 OpenAI test 파일 1,000줄 이하.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/04_core_config_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/04_core_config_tests, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/04_core_config_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] config 영역별 test 파일 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] config_test.go를 Edge/OpenAI/CLI/provider/Node 영역별 파일로 옮기고 Test 107개, 원본 제거, package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- config schema와 production source가 변경되지 않았는가.
|
||||
- Test 107개와 YAML fixture assertion이 보존되었는가.
|
||||
- 각 test 파일이 config 책임과 대응하고 1,000줄 이하인가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w packages/go/config/*_test.go
|
||||
$ test ! -e packages/go/config/config_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./packages/go/config
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
$ python3 -c "from pathlib import Path; p=Path('packages/go/config'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/04_core_config_tests plan=0 tag=TEST -->
|
||||
|
||||
# Core config 테스트 책임별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 검증을 실행하고 CODE_REVIEW 파일의 구현 에이전트 소유 섹션에 실제 메모와 출력을 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log, archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 CODE_REVIEW의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 request_user_input을 호출하거나 USER_REVIEW.md/complete.log를 만들지 않는다. 환경·secret·service, 일반 범위 조정, 후속 에이전트가 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
packages/go/config/config_test.go는 3,802줄에서 Edge와 Node, OpenAI, CLI profile, provider catalog 검증을 함께 담고 있다. config type의 기존 package 경계를 유지하면서 책임별 test 파일로 재배치한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 기록한다. 섹션 형식은 agent-ops/skills/common/_templates/implementation-user-review-request-section.md를 따른다. 구현 중 직접 사용자 prompt는 금지하며, 요청의 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/platform-common-smoke.md
|
||||
- agent-spec/runtime/provider-pool-config-refresh.md
|
||||
- packages/go/config/config_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 platform-common-smoke profile을 읽었다. 현재 checkout에서 GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./packages/go/config가 통과했다. fresh package test와 Test 목록 107개를 계약으로 사용한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
config 동작 변경은 없다. 기존 107개 top-level Test의 YAML fixture와 assertion을 그대로 이동한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 그대로 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
공유 task group 아래 독립 04_core_config_tests다. 05_core_node_tests도 독립이며 06+04,05_core_edge_tests가 두 predecessor 완료 뒤 core-tests를 최종 완료한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
packages/go/config/config_test.go와 test-only helper만 수정한다. config.go 및 외부 config schema는 바꾸지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G06: 단일 package의 대형 테스트 이동이며 영역별 import/fixture 정리가 필요하지만 동작은 완전 고정이다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] config_test.go를 Edge/OpenAI/CLI/provider/Node 영역별 파일로 옮기고 Test 107개, 원본 제거, package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] config 영역별 test 파일 분리
|
||||
|
||||
- 문제: packages/go/config/config_test.go:14부터 3,802줄까지 Edge identity/OpenAI/CLI/provider/Node config가 한 파일에 결합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
packages/go/config/config_test.go:14 func TestLoadEdge_EdgeIdentity(...)
|
||||
packages/go/config/config_test.go:722 func TestLoadEdge_CodexProfile(...)
|
||||
packages/go/config/config_test.go:1474 func TestLoadEdge_MultiOllamaInstances(...)
|
||||
|
||||
After
|
||||
config_test_helpers_test.go YAML/temp helper
|
||||
edge_openai_config_test.go Edge identity/OpenAI
|
||||
edge_cli_config_test.go console/CLI profiles
|
||||
edge_runtime_config_test.go refresh/control-plane/A2A
|
||||
provider_catalog_config_test.go provider instances/routes
|
||||
node_config_test.go Node config
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] packages/go/config/config_test.go: 모든 테스트를 이동한 뒤 삭제한다.
|
||||
- [ ] packages/go/config/config_test_helpers_test.go: 반복 YAML/temp helper를 둔다.
|
||||
- [ ] packages/go/config/edge_openai_config_test.go: Edge/OpenAI 설정 테스트를 둔다.
|
||||
- [ ] packages/go/config/edge_cli_config_test.go: console/CLI profile 테스트를 둔다.
|
||||
- [ ] packages/go/config/edge_runtime_config_test.go: runtime/refresh/control-plane 테스트를 둔다.
|
||||
- [ ] packages/go/config/provider_catalog_config_test.go: provider/catalog/route 테스트를 둔다.
|
||||
- [ ] packages/go/config/node_config_test.go: Node 설정 테스트를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. config schema를 바꾸지 않는 internal test topology refactor이고 기존 Test 107개를 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w packages/go/config/*_test.go
|
||||
test ! -e packages/go/config/config_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 원본 삭제, gofmt 성공, Test 107개.
|
||||
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| packages/go/config/config_test.go | TEST-1 |
|
||||
| packages/go/config/config_test_helpers_test.go | TEST-1 |
|
||||
| packages/go/config/edge_openai_config_test.go | TEST-1 |
|
||||
| packages/go/config/edge_cli_config_test.go | TEST-1 |
|
||||
| packages/go/config/edge_runtime_config_test.go | TEST-1 |
|
||||
| packages/go/config/provider_catalog_config_test.go | TEST-1 |
|
||||
| packages/go/config/node_config_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./packages/go/config
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
python3 -c "from pathlib import Path; p=Path('packages/go/config'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: fresh config test PASS, Test 107개, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/05_core_node_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/05_core_node_tests, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/05_core_node_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Node run/command/refresh/tunnel 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] node_test.go를 run/cancel, command, refresh, tunnel 파일로 옮기고 Test 66개, 원본 제거, package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Node production behavior와 timing assertion이 변경되지 않았는가.
|
||||
- Test 66개가 보존되었는가.
|
||||
- run/command/refresh/tunnel 파일과 helper 경계가 명확한가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/node/internal/node/*_test.go
|
||||
$ test ! -e apps/node/internal/node/node_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/node
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
$ python3 -c "from pathlib import Path; p=Path('apps/node/internal/node'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/05_core_node_tests plan=0 tag=TEST -->
|
||||
|
||||
# Core Node 테스트 책임별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
apps/node/internal/node/node_test.go는 3,346줄에서 run/cancel, command, refresh, tunnel을 함께 검증한다. Node package 경계를 유지하면서 실행 책임별 파일과 최소 fixture로 나눈다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/node-smoke.md
|
||||
- agent-spec/runtime/edge-node-execution.md
|
||||
- agent-spec/runtime/provider-pool-config-refresh.md
|
||||
- apps/node/internal/node/node_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 node-smoke profile을 읽었다. 현재 checkout에서 GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/node가 통과했다. cache를 쓰지 않는 package test와 Test 목록 66개를 계약으로 사용한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
Node 동작 변경은 없다. 기존 66개 top-level Test가 run/cancel/command/refresh/tunnel 경로를 덮으며 모두 그대로 이동한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
독립 05_core_node_tests다. 04_core_config_tests도 독립이고 06+04,05_core_edge_tests가 두 complete.log 뒤 core-tests를 최종 완료한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
node_test.go와 test-only helper만 수정한다. node.go, wire, adapter API는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 3,300줄, concurrency/refresh/tunnel fixture를 책임별로 옮기며 공유 double 축소가 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] node_test.go를 run/cancel, command, refresh, tunnel 파일로 옮기고 Test 66개, 원본 제거, package test 통과를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Node run/command/refresh/tunnel 테스트 분리
|
||||
|
||||
- 문제: apps/node/internal/node/node_test.go:283~3346의 run/cancel, command, refresh, tunnel 시나리오가 공통 fake와 한 파일에 결합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/node/internal/node/node_test.go:283 func TestOnRunRequest_RouterError(...)
|
||||
apps/node/internal/node/node_test.go:501 func TestOnCommandRequest_Success(...)
|
||||
apps/node/internal/node/node_test.go:1563 func TestNodeConfigRefreshWithoutApplyManagerReportsRestartRequired(...)
|
||||
apps/node/internal/node/node_test.go:3097 func TestNodeOnProviderTunnelRequest_Success(...)
|
||||
|
||||
After
|
||||
node_test_support_test.go 최소 registry/adapter fixture
|
||||
run_cancel_test.go run/cancel/terminal event
|
||||
command_test.go command/capabilities
|
||||
gate_refresh_test.go admission/config refresh
|
||||
provider_tunnel_test.go provider tunnel
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/node/internal/node/node_test.go: 모든 테스트를 이동한 뒤 삭제한다.
|
||||
- [ ] apps/node/internal/node/node_test_support_test.go: 공통 최소 fixture를 둔다.
|
||||
- [ ] apps/node/internal/node/run_cancel_test.go: run/cancel/terminal event 테스트를 둔다.
|
||||
- [ ] apps/node/internal/node/command_test.go: command/capabilities 테스트를 둔다.
|
||||
- [ ] apps/node/internal/node/gate_refresh_test.go: admission/config refresh 테스트를 둔다.
|
||||
- [ ] apps/node/internal/node/provider_tunnel_test.go: tunnel 테스트를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. concurrency assertion과 timing 값을 포함한 기존 Test 66개를 그대로 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/node/internal/node/*_test.go
|
||||
test ! -e apps/node/internal/node/node_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 원본 삭제, gofmt 성공, Test 66개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/node/internal/node/node_test.go | TEST-1 |
|
||||
| apps/node/internal/node/node_test_support_test.go | TEST-1 |
|
||||
| apps/node/internal/node/run_cancel_test.go | TEST-1 |
|
||||
| apps/node/internal/node/command_test.go | TEST-1 |
|
||||
| apps/node/internal/node/gate_refresh_test.go | TEST-1 |
|
||||
| apps/node/internal/node/provider_tunnel_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/node
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
python3 -c "from pathlib import Path; p=Path('apps/node/internal/node'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: fresh Node test PASS, Test 66개, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/06+04,05_core_edge_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/06+04,05_core_edge_tests, plan=0, tag=TEST
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `core-tests`: config, Node, Edge service/bootstrap 테스트를 책임별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G08.md` → `code_review_local_G08_N.log`, `PLAN-local-G08.md` → `plan_local_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/06+04,05_core_edge_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Edge queue/service/bootstrap 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] service/bootstrap 대형 테스트를 scheduling/snapshot/runtime refresh 파일로 옮기고 core 네 package fresh test 및 Test 수를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 04와 05 predecessor complete.log가 모두 존재했는가.
|
||||
- queue/concurrency/runtime refresh assertion과 네 package Test 수가 보존되었는가.
|
||||
- 세 원본이 제거되고 1,000줄 초과 test가 없는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/edge/internal/service/*_test.go apps/edge/internal/bootstrap/*_test.go
|
||||
$ test ! -e apps/edge/internal/service/model_queue_test.go
|
||||
$ test ! -e apps/edge/internal/service/service_test.go
|
||||
$ test ! -e apps/edge/internal/bootstrap/runtime_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/service | sed '/^ok[[:space:]]/d' | wc -l)" -eq 96 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/bootstrap | sed '/^ok[[:space:]]/d' | wc -l)" -eq 21 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./packages/go/config ./apps/node/internal/node ./apps/edge/internal/service ./apps/edge/internal/bootstrap
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/service | sed '/^ok[[:space:]]/d' | wc -l)" -eq 96 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/bootstrap | sed '/^ok[[:space:]]/d' | wc -l)" -eq 21 ]
|
||||
$ python3 -c "from pathlib import Path; roots=[Path('packages/go/config'),Path('apps/node/internal/node'),Path('apps/edge/internal/service'),Path('apps/edge/internal/bootstrap')]; bad=sorted((sum(1 for _ in f.open()),str(f)) for p in roots for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/06+04,05_core_edge_tests plan=0 tag=TEST -->
|
||||
|
||||
# Core Edge service/bootstrap 테스트 책임별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
core-tests의 마지막 범위는 Edge service의 queue/scheduling/snapshot과 bootstrap runtime refresh다. config와 Node 분해 완료 뒤 세 대형 파일을 package별 책임 파일로 옮기고 core 전체 회귀를 함께 확인한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `core-tests`: config, Node, Edge service/bootstrap 테스트를 책임별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-test/local/profiles/platform-common-smoke.md
|
||||
- agent-test/local/profiles/node-smoke.md
|
||||
- agent-spec/runtime/edge-node-execution.md
|
||||
- agent-spec/runtime/provider-pool-config-refresh.md
|
||||
- apps/edge/internal/service/model_queue_test.go
|
||||
- apps/edge/internal/service/service_test.go
|
||||
- apps/edge/internal/bootstrap/runtime_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 edge/platform-common/node smoke profile을 읽었다. 현재 checkout에서 core 대상 네 package의 fresh test가 통과했다. Test 수 config=107, node=66, service=96, bootstrap=21을 보존한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. queue ordering, concurrency, refresh, connector 테스트를 그대로 이동하며 package별 Test 수와 fresh test로 누락을 검출한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 디렉터리의 +04,05는 04_core_config_tests와 05_core_node_tests의 complete.log를 모두 요구한다. 현재 둘 다 미충족이다. 이 plan만 core-tests Roadmap Target을 check-on-pass 한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
apps/edge/internal/service와 bootstrap의 세 대형 test 파일/test helper만 수정한다. service/runtime production code와 E2E는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G08: 5,700줄 이상, 두 package, queue/concurrency/runtime refresh fixture와 최종 core completion을 다룬다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] service/bootstrap 대형 테스트를 scheduling/snapshot/runtime refresh 파일로 옮기고 core 네 package fresh test 및 Test 수를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Edge queue/service/bootstrap 테스트 분리
|
||||
|
||||
- 문제: model_queue_test.go:43~2365, service_test.go:23~1595, runtime_test.go:41~1803에 admission/scheduling/snapshot/runtime refresh가 파일별로 다시 혼합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/edge/internal/service/model_queue_test.go:43 func TestModelQueueFIFOOrdering(...)
|
||||
apps/edge/internal/service/service_test.go:951 func TestSubmitRunModelQueueDispatchesQueuedRunAfterDisconnectToLiveNode(...)
|
||||
apps/edge/internal/bootstrap/runtime_test.go:187 func TestRefreshAdminDryRunAndApplyReachRunningRuntime(...)
|
||||
|
||||
After
|
||||
service/model_queue_admission_test.go
|
||||
service/provider_scheduling_test.go
|
||||
service/long_context_queue_test.go
|
||||
service/run_command_test.go
|
||||
service/queue_dispatch_test.go
|
||||
bootstrap/runtime_lifecycle_test.go
|
||||
bootstrap/runtime_refresh_test.go
|
||||
bootstrap/runtime_connector_test.go
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/edge/internal/service/model_queue_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/edge/internal/service/service_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/edge/internal/bootstrap/runtime_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/edge/internal/service/model_queue_test_support_test.go: queue fixture를 둔다.
|
||||
- [ ] apps/edge/internal/service/model_queue_admission_test.go: FIFO/overflow/release를 둔다.
|
||||
- [ ] apps/edge/internal/service/provider_scheduling_test.go: provider pool/snapshot을 둔다.
|
||||
- [ ] apps/edge/internal/service/long_context_queue_test.go: long-context admission을 둔다.
|
||||
- [ ] apps/edge/internal/service/run_command_test.go: request/command/capability를 둔다.
|
||||
- [ ] apps/edge/internal/service/queue_dispatch_test.go: submit queue dispatch를 둔다.
|
||||
- [ ] apps/edge/internal/bootstrap/runtime_test_support_test.go: runtime fixture를 둔다.
|
||||
- [ ] apps/edge/internal/bootstrap/runtime_lifecycle_test.go: lifecycle/wiring을 둔다.
|
||||
- [ ] apps/edge/internal/bootstrap/runtime_refresh_test.go: refresh apply/reject를 둔다.
|
||||
- [ ] apps/edge/internal/bootstrap/runtime_connector_test.go: control-plane connector를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. existing concurrency/order assertions를 유지하고 네 package의 Test 수와 fresh 실행으로 검증한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/internal/service/*_test.go apps/edge/internal/bootstrap/*_test.go
|
||||
test ! -e apps/edge/internal/service/model_queue_test.go
|
||||
test ! -e apps/edge/internal/service/service_test.go
|
||||
test ! -e apps/edge/internal/bootstrap/runtime_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/service | sed '/^ok[[:space:]]/d' | wc -l)" -eq 96 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/bootstrap | sed '/^ok[[:space:]]/d' | wc -l)" -eq 21 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 세 원본 삭제, service Test 96개, bootstrap Test 21개.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. agent-task/m-agent-readable-repository-refactor/04_core_config_tests/complete.log를 확인한다.
|
||||
2. agent-task/m-agent-readable-repository-refactor/05_core_node_tests/complete.log를 확인한다.
|
||||
3. 디렉터리 이름에 없는 추가 predecessor는 요구하지 않는다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/edge/internal/service/model_queue_test.go | TEST-1 |
|
||||
| apps/edge/internal/service/service_test.go | TEST-1 |
|
||||
| apps/edge/internal/service/*queue*_test.go | TEST-1 |
|
||||
| apps/edge/internal/service/*scheduling*_test.go | TEST-1 |
|
||||
| apps/edge/internal/service/run_command_test.go | TEST-1 |
|
||||
| apps/edge/internal/bootstrap/runtime_test.go | TEST-1 |
|
||||
| apps/edge/internal/bootstrap/runtime_*_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./packages/go/config ./apps/node/internal/node ./apps/edge/internal/service ./apps/edge/internal/bootstrap
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./packages/go/config | sed '/^ok[[:space:]]/d' | wc -l)" -eq 107 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/node | sed '/^ok[[:space:]]/d' | wc -l)" -eq 66 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/service | sed '/^ok[[:space:]]/d' | wc -l)" -eq 96 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/bootstrap | sed '/^ok[[:space:]]/d' | wc -l)" -eq 21 ]
|
||||
python3 -c "from pathlib import Path; roots=[Path('packages/go/config'),Path('apps/node/internal/node'),Path('apps/edge/internal/service'),Path('apps/edge/internal/bootstrap')]; bad=sorted((sum(1 for _ in f.open()),str(f)) for p in roots for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: core 네 package fresh test PASS, Test 수 보존, 사유 없는 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/07_adapter_openai_compat_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/07_adapter_openai_compat_tests, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G06.md` → `code_review_local_G06_N.log`, `PLAN-local-G06.md` → `plan_local_G06_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/07_adapter_openai_compat_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] OpenAI-compatible adapter 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] openai_compat_test.go를 capabilities/execute/thinking/tunnel 파일로 옮기고 Test 32개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- HTTP fixture와 Test 32개가 보존되었는가.
|
||||
- thinking/tunnel 책임 경계가 명확한가.
|
||||
- 원본 제거와 1,000줄 gate가 통과하는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/node/internal/adapters/openai_compat/*_test.go
|
||||
$ test ! -e apps/node/internal/adapters/openai_compat/openai_compat_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/openai_compat | sed '/^ok[[:space:]]/d' | wc -l)" -eq 32 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/openai_compat
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/openai_compat | sed '/^ok[[:space:]]/d' | wc -l)" -eq 32 ]
|
||||
$ python3 -c "from pathlib import Path; p=Path('apps/node/internal/adapters/openai_compat'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/07_adapter_openai_compat_tests plan=0 tag=TEST -->
|
||||
|
||||
# OpenAI-compatible adapter 테스트 실행 책임별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
openai_compat_test.go는 1,647줄에서 capabilities, chat execute, thinking mapping, provider tunnel을 함께 검증한다. adapter package 내부에서 실행 경로별 test 파일로 나눈다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/node-smoke.md
|
||||
- agent-spec/input/openai-compatible-surface.md
|
||||
- apps/node/internal/adapters/openai_compat/openai_compat_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 node-smoke profile을 읽었다. 현재 checkout에서 fresh openai_compat package test가 통과했고 Test 목록은 32개다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 HTTP fixtures와 32개 Test를 그대로 이동한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
adapter-command-tests는 adapter 종류와 runner 성격에 따라 분할한다. 07_adapter_openai_compat_tests, 08_adapter_cli_tests, 09_adapter_edge_command_tests는 독립이며 10+07,08,09_adapter_control_plane_tests가 최종 완료한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
openai_compat_test.go와 test-only helper만 수정한다. adapter production code와 OpenAI Edge server는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G06: 1,600줄 단일 package의 명확한 기계적 이동이다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] openai_compat_test.go를 capabilities/execute/thinking/tunnel 파일로 옮기고 Test 32개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] OpenAI-compatible adapter 테스트 분리
|
||||
|
||||
- 문제: apps/node/internal/adapters/openai_compat/openai_compat_test.go:40~1647에 네 실행 책임이 한 파일에 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
openai_compat_test.go:40 func TestOpenAICompatCapabilitiesQueryModels(...)
|
||||
openai_compat_test.go:164 func TestOpenAICompatExecuteStreamsDeltasAndFinishReason(...)
|
||||
openai_compat_test.go:657 func TestOpenAICompatExecuteMapsThinkFalseForVLLMChatTemplateKwargs(...)
|
||||
openai_compat_test.go:1202 func TestOpenAICompatTunnelProvider(...)
|
||||
|
||||
After
|
||||
openai_compat_test_support_test.go
|
||||
capabilities_test.go
|
||||
execute_test.go
|
||||
thinking_policy_test.go
|
||||
provider_tunnel_test.go
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/node/internal/adapters/openai_compat/openai_compat_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/node/internal/adapters/openai_compat/openai_compat_test_support_test.go: HTTP/stream fixture를 둔다.
|
||||
- [ ] apps/node/internal/adapters/openai_compat/capabilities_test.go: capabilities/probe를 둔다.
|
||||
- [ ] apps/node/internal/adapters/openai_compat/execute_test.go: chat execute/tool을 둔다.
|
||||
- [ ] apps/node/internal/adapters/openai_compat/thinking_policy_test.go: think/reasoning mapping을 둔다.
|
||||
- [ ] apps/node/internal/adapters/openai_compat/provider_tunnel_test.go: tunnel relay/cancel을 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. 기존 32개 Test, httptest fixture, assertion을 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/node/internal/adapters/openai_compat/*_test.go
|
||||
test ! -e apps/node/internal/adapters/openai_compat/openai_compat_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/openai_compat | sed '/^ok[[:space:]]/d' | wc -l)" -eq 32 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 원본 삭제, gofmt 성공, Test 32개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/node/internal/adapters/openai_compat/openai_compat_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/openai_compat/openai_compat_test_support_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/openai_compat/capabilities_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/openai_compat/execute_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/openai_compat/thinking_policy_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/openai_compat/provider_tunnel_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/openai_compat
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/openai_compat | sed '/^ok[[:space:]]/d' | wc -l)" -eq 32 ]
|
||||
python3 -c "from pathlib import Path; p=Path('apps/node/internal/adapters/openai_compat'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: fresh adapter test PASS, Test 32개, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/08_adapter_cli_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/08_adapter_cli_tests, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G08.md` → `code_review_local_G08_N.log`, `PLAN-local-G08.md` → `plan_local_G08_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/08_adapter_cli_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] CLI/PTY/Codex app-server 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] CLI emitter/session, persistent PTY, Codex app-server 테스트를 실행 모드별 파일로 옮기고 Test 173개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- PTY timing/helper process 의미와 Test 173개가 보존되었는가.
|
||||
- 2분 무진행 대응 증거가 필요한 경우 기록되었는가.
|
||||
- 세 원본 제거와 1,000줄 gate가 통과하는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/node/internal/adapters/cli/*_test.go
|
||||
$ test ! -e apps/node/internal/adapters/cli/cli_internal_test.go
|
||||
$ test ! -e apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go
|
||||
$ test ! -e apps/node/internal/adapters/cli/codex_app_server_internal_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/cli | sed '/^ok[[:space:]]/d' | wc -l)" -eq 173 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/cli
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/cli | sed '/^ok[[:space:]]/d' | wc -l)" -eq 173 ]
|
||||
$ python3 -c "from pathlib import Path; p=Path('apps/node/internal/adapters/cli'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/08_adapter_cli_tests plan=0 tag=TEST -->
|
||||
|
||||
# CLI adapter 테스트 실행 모드별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
CLI adapter에는 1,110~1,462줄의 세 test 파일이 emitter/session, persistent PTY, Codex app-server 책임을 다시 혼합한다. terminal semantics를 바꾸지 않고 mode/session/event 책임별 파일로 재배치한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/node-smoke.md
|
||||
- agent-spec/runtime/edge-node-execution.md
|
||||
- apps/node/internal/adapters/cli/cli_internal_test.go
|
||||
- apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go
|
||||
- apps/node/internal/adapters/cli/codex_app_server_internal_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 테스트 재배치라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 node-smoke profile을 읽었다. 현재 checkout에서 fresh CLI package test가 46.960초에 통과했고 Test 목록은 173개다. PTY test이므로 2분 무진행 시 blind retry를 중단하고 focused -run 출력과 환경 근거를 남긴다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. 기존 173개 Test의 PTY timing, helper process, emitter assertion을 그대로 이동한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 Test* 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
CLI/PTY는 openai_compat HTTP adapter와 risk/verification이 달라 독립 08 plan이다. 07,08,09는 병렬 가능하고 10+07,08,09가 최종 adapter-command-tests를 완료한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
세 1,000줄 이상 CLI test 파일과 test-only helper만 수정한다. process/session production code, timeout 값, terminal behavior는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G08: 4,000줄 가까운 PTY/helper-process test를 여러 책임으로 옮기며 import와 helper 가시성 보존이 중요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] CLI emitter/session, persistent PTY, Codex app-server 테스트를 실행 모드별 파일로 옮기고 Test 173개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] CLI/PTY/Codex app-server 테스트 분리
|
||||
|
||||
- 문제: cli_internal_test.go:41, persistent_execute_blackbox_test.go:21, codex_app_server_internal_test.go:33부터 서로 다른 실행 모드와 helper가 각각 대형 파일에 혼합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
cli_internal_test.go:41 func TestDriveJSONLines_DispatchesEmitterEvents(...)
|
||||
persistent_execute_blackbox_test.go:21 func TestCLIExecutePersistentWaitsForSlowFirstOutput(...)
|
||||
codex_app_server_internal_test.go:33 func TestCodexAppServerSessionResolve(...)
|
||||
|
||||
After
|
||||
cli_test_support_test.go
|
||||
cli_emitters_test.go
|
||||
cli_session_test.go
|
||||
cli_workspace_test.go
|
||||
persistent_process_test.go
|
||||
persistent_terminal_test.go
|
||||
persistent_completion_test.go
|
||||
codex_app_server_session_test.go
|
||||
codex_app_server_events_test.go
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/node/internal/adapters/cli/cli_internal_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/node/internal/adapters/cli/codex_app_server_internal_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/node/internal/adapters/cli/cli_test_support_test.go: helper process와 공통 fixture를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/cli_emitters_test.go: JSON emitter/driver를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/cli_session_test.go: session list/terminate를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/cli_workspace_test.go: cwd/preflight를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/persistent_process_test.go: non-terminal persistent를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/persistent_terminal_test.go: PTY/TUI를 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/persistent_completion_test.go: cancel/marker/idle을 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/codex_app_server_session_test.go: init/thread/turn을 둔다.
|
||||
- [ ] apps/node/internal/adapters/cli/codex_app_server_events_test.go: event/drain/priority를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. existing PTY/helper process Test 173개와 timing을 그대로 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/node/internal/adapters/cli/*_test.go
|
||||
test ! -e apps/node/internal/adapters/cli/cli_internal_test.go
|
||||
test ! -e apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go
|
||||
test ! -e apps/node/internal/adapters/cli/codex_app_server_internal_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/cli | sed '/^ok[[:space:]]/d' | wc -l)" -eq 173 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 세 원본 삭제, gofmt 성공, Test 173개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/node/internal/adapters/cli/cli_internal_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/cli/persistent_execute_blackbox_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/cli/codex_app_server_internal_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/cli/cli_*_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/cli/persistent_*_test.go | TEST-1 |
|
||||
| apps/node/internal/adapters/cli/codex_app_server_*_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/cli
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/node/internal/adapters/cli | sed '/^ok[[:space:]]/d' | wc -l)" -eq 173 ]
|
||||
python3 -c "from pathlib import Path; p=Path('apps/node/internal/adapters/cli'); bad=sorted((sum(1 for _ in f.open()),str(f)) for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: fresh CLI test PASS, Test 173개, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/09_adapter_edge_command_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/09_adapter_edge_command_tests, plan=0, tag=TEST
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/09_adapter_edge_command_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Edge command/config refresh 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Edge command와 config-refresh 테스트를 command/classification 책임별 파일로 옮기고 Test 30/17개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- command output/temp fixture와 Test 30개가 보존되었는가.
|
||||
- refresh classification Test 17개가 보존되었는가.
|
||||
- 두 원본 제거와 1,000줄 gate가 통과하는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/edge/cmd/edge/*_test.go apps/edge/internal/configrefresh/*_test.go
|
||||
$ test ! -e apps/edge/cmd/edge/main_test.go
|
||||
$ test ! -e apps/edge/internal/configrefresh/classify_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/cmd/edge | sed '/^ok[[:space:]]/d' | wc -l)" -eq 30 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/configrefresh | sed '/^ok[[:space:]]/d' | wc -l)" -eq 17 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/configrefresh
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/cmd/edge | sed '/^ok[[:space:]]/d' | wc -l)" -eq 30 ]
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/configrefresh | sed '/^ok[[:space:]]/d' | wc -l)" -eq 17 ]
|
||||
$ python3 -c "from pathlib import Path; roots=[Path('apps/edge/cmd/edge'),Path('apps/edge/internal/configrefresh')]; bad=sorted((sum(1 for _ in f.open()),str(f)) for p in roots for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/09_adapter_edge_command_tests plan=0 tag=TEST -->
|
||||
|
||||
# Edge command/config-refresh 테스트 책임별 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
apps/edge/cmd/edge/main_test.go와 internal/configrefresh/classify_test.go는 각각 1,383줄과 1,314줄이다. command workflow와 refresh classification을 책임별 파일로 옮겨 변경 시 필요한 테스트 read set을 줄인다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-spec/runtime/provider-pool-config-refresh.md
|
||||
- apps/edge/cmd/edge/main_test.go
|
||||
- apps/edge/internal/configrefresh/classify_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 리팩터링이라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 edge-smoke profile을 읽었다. 현재 checkout에서 두 package fresh test가 통과했고 Test 목록은 edge cmd=30, configrefresh=17이다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. temp config, CLI output, refresh classification assertion을 그대로 이동하고 Test 수를 보존한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 테스트 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
07_adapter_openai_compat_tests, 08_adapter_cli_tests, 09_adapter_edge_command_tests는 독립이다. 10+07,08,09_adapter_control_plane_tests가 세 complete.log 뒤 adapter-command-tests를 완료한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
두 대형 test 파일과 test-only helper만 수정한다. Cobra command와 refresh production code, runtime smoke는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 두 package 2,700줄의 파일 이동이며 CLI temp artifact fixture와 config classification grouping이 필요하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Edge command와 config-refresh 테스트를 command/classification 책임별 파일로 옮기고 Test 30/17개와 fresh package test를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Edge command/config refresh 테스트 분리
|
||||
|
||||
- 문제: apps/edge/cmd/edge/main_test.go:19~1383과 apps/edge/internal/configrefresh/classify_test.go:95~1314가 서로 다른 command/refresh 시나리오를 각각 한 파일에 담는다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
apps/edge/cmd/edge/main_test.go:19 func TestRootCmdIncludesOperationalCommands(...)
|
||||
apps/edge/cmd/edge/main_test.go:347 func TestBootstrapPackCreatesNodeArtifacts(...)
|
||||
apps/edge/internal/configrefresh/classify_test.go:95 func TestClassifyProviderCapacityApplied(...)
|
||||
|
||||
After
|
||||
cmd/edge/main_test_support_test.go
|
||||
cmd/edge/root_config_command_test.go
|
||||
cmd/edge/bootstrap_node_command_test.go
|
||||
cmd/edge/smoke_command_test.go
|
||||
configrefresh/classify_test_support_test.go
|
||||
configrefresh/provider_classify_test.go
|
||||
configrefresh/node_runtime_classify_test.go
|
||||
configrefresh/path_refresh_test.go
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/edge/cmd/edge/main_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/edge/cmd/edge/main_test_support_test.go: temp config/command helper를 둔다.
|
||||
- [ ] apps/edge/cmd/edge/root_config_command_test.go: root/config/setup/env를 둔다.
|
||||
- [ ] apps/edge/cmd/edge/bootstrap_node_command_test.go: bootstrap/node register를 둔다.
|
||||
- [ ] apps/edge/cmd/edge/smoke_command_test.go: smoke command를 둔다.
|
||||
- [ ] apps/edge/internal/configrefresh/classify_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/edge/internal/configrefresh/classify_test_support_test.go: fixture를 둔다.
|
||||
- [ ] apps/edge/internal/configrefresh/provider_classify_test.go: provider 분류를 둔다.
|
||||
- [ ] apps/edge/internal/configrefresh/node_runtime_classify_test.go: Node/runtime을 둔다.
|
||||
- [ ] apps/edge/internal/configrefresh/path_refresh_test.go: logging/path/dry-run을 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. 기존 Test 30/17개와 output/assertion을 그대로 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/edge/cmd/edge/*_test.go apps/edge/internal/configrefresh/*_test.go
|
||||
test ! -e apps/edge/cmd/edge/main_test.go
|
||||
test ! -e apps/edge/internal/configrefresh/classify_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/cmd/edge | sed '/^ok[[:space:]]/d' | wc -l)" -eq 30 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/configrefresh | sed '/^ok[[:space:]]/d' | wc -l)" -eq 17 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 두 원본 삭제, gofmt 성공, Test 30/17개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/edge/cmd/edge/main_test.go | TEST-1 |
|
||||
| apps/edge/cmd/edge/*command*_test.go | TEST-1 |
|
||||
| apps/edge/cmd/edge/main_test_support_test.go | TEST-1 |
|
||||
| apps/edge/internal/configrefresh/classify_test.go | TEST-1 |
|
||||
| apps/edge/internal/configrefresh/*classify*_test.go | TEST-1 |
|
||||
| apps/edge/internal/configrefresh/path_refresh_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/configrefresh
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/cmd/edge | sed '/^ok[[:space:]]/d' | wc -l)" -eq 30 ]
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/edge/internal/configrefresh | sed '/^ok[[:space:]]/d' | wc -l)" -eq 17 ]
|
||||
python3 -c "from pathlib import Path; roots=[Path('apps/edge/cmd/edge'),Path('apps/edge/internal/configrefresh')]; bad=sorted((sum(1 for _ in f.open()),str(f)) for p in roots for f in p.glob('*_test.go') if sum(1 for _ in f.open())>1000); print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: 두 package fresh test PASS, Test 수 보존, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/10+07,08,09_adapter_control_plane_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/10+07,08,09_adapter_control_plane_tests, plan=0, tag=TEST
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `adapter-command-tests`: adapter, CLI, Edge/Control Plane command와 config refresh 테스트를 실행 책임별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/10+07,08,09_adapter_control_plane_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] Control Plane config/registry/fleet 테스트 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Control Plane config/registry/fleet 테스트를 분리하고 모든 adapter-command 대상 fresh test와 1,000줄 gate를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 07,08,09 predecessor complete.log가 모두 존재했는가.
|
||||
- Control Plane Test 24개와 handler concurrency/cache assertion이 보존되었는가.
|
||||
- adapter-command 대상 전체 fresh test와 LOC gate가 통과하는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ gofmt -w apps/control-plane/cmd/control-plane/*_test.go
|
||||
$ test ! -e apps/control-plane/cmd/control-plane/main_test.go
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/control-plane/cmd/control-plane | sed '/^ok[[:space:]]/d' | wc -l)" -eq 24 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/cli ./apps/edge/cmd/edge ./apps/edge/internal/configrefresh ./apps/control-plane/cmd/control-plane
|
||||
$ [ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/control-plane/cmd/control-plane | sed '/^ok[[:space:]]/d' | wc -l)" -eq 24 ]
|
||||
$ python3 -c "from pathlib import Path; roots=[Path('apps/node/internal/adapters'),Path('apps/edge/cmd/edge'),Path('apps/edge/internal/configrefresh'),Path('apps/control-plane/cmd/control-plane')]; fs=sorted(f for r in roots for f in r.rglob('*_test.go')); bad=[(sum(1 for _ in f.open()),str(f)) for f in fs if sum(1 for _ in f.open())>1000]; print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/10+07,08,09_adapter_control_plane_tests plan=0 tag=TEST -->
|
||||
|
||||
# Control Plane command 테스트 분해와 adapter-command 완료
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
adapter-command-tests의 마지막 범위는 Control Plane main_test.go의 config, edge registry, fleet handler 테스트다. 세 선행 adapter/Edge command 분해가 끝난 뒤 이를 분리하고 전체 대상에서 사유 없는 1,000줄 초과가 남지 않았는지 확인한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `adapter-command-tests`: adapter, CLI, Edge/Control Plane command와 config refresh 테스트를 실행 책임별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/control-plane-smoke.md
|
||||
- agent-test/local/profiles/edge-smoke.md
|
||||
- agent-test/local/profiles/node-smoke.md
|
||||
- agent-spec/control/control-plane-operations.md
|
||||
- apps/control-plane/cmd/control-plane/main_test.go
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 리팩터링이라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 control-plane/edge/node smoke profile을 읽었다. 현재 checkout에서 adapter-command 대상 package가 모두 fresh PASS했고 Control Plane Test 목록은 24개다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. config credential redaction, registry HTTP, fleet concurrency/cache assertion을 그대로 이동한다. final command가 모든 선행 package도 재검증한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 테스트 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 디렉터리의 +07,08,09는 세 독립 predecessor의 complete.log를 모두 요구하며 현재 미충족이다. 이 plan만 adapter-command-tests Roadmap Target을 check-on-pass 한다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Control Plane main_test.go와 test-only support를 수정한다. server production code, DB/Redis integration, 외부 runtime은 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 1,226줄 분해 자체는 명확하지만 세 선행 작업의 전체 package/LOC completion 검증을 소유한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] Control Plane config/registry/fleet 테스트를 분리하고 모든 adapter-command 대상 fresh test와 1,000줄 gate를 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] Control Plane config/registry/fleet 테스트 분리
|
||||
|
||||
- 문제: apps/control-plane/cmd/control-plane/main_test.go:23~1226에 config/env/log redaction, edge registry HTTP, fleet fanout/cache가 한 파일에 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
main_test.go:23 func TestLoadConfigDatabaseDefaultsToLocal(...)
|
||||
main_test.go:258 func TestEdgeRegistryHTTPHandlersListAndGetEdge(...)
|
||||
main_test.go:864 func TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities(...)
|
||||
|
||||
After
|
||||
main_test_support_test.go
|
||||
config_test.go
|
||||
edge_registry_handler_test.go
|
||||
fleet_handler_test.go
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/control-plane/cmd/control-plane/main_test.go: 이동 후 삭제한다.
|
||||
- [ ] apps/control-plane/cmd/control-plane/main_test_support_test.go: registry/HTTP fixture를 둔다.
|
||||
- [ ] apps/control-plane/cmd/control-plane/config_test.go: config/env/log redaction을 둔다.
|
||||
- [ ] apps/control-plane/cmd/control-plane/edge_registry_handler_test.go: edge HTTP handler를 둔다.
|
||||
- [ ] apps/control-plane/cmd/control-plane/fleet_handler_test.go: fleet status/command/cache/concurrency를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. 기존 Control Plane Test 24개와 모든 선행 Test 이름/assertion을 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
gofmt -w apps/control-plane/cmd/control-plane/*_test.go
|
||||
test ! -e apps/control-plane/cmd/control-plane/main_test.go
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/control-plane/cmd/control-plane | sed '/^ok[[:space:]]/d' | wc -l)" -eq 24 ]
|
||||
~~~
|
||||
|
||||
기대 결과: 원본 삭제, gofmt 성공, Control Plane Test 24개.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. agent-task/m-agent-readable-repository-refactor/07_adapter_openai_compat_tests/complete.log를 확인한다.
|
||||
2. agent-task/m-agent-readable-repository-refactor/08_adapter_cli_tests/complete.log를 확인한다.
|
||||
3. agent-task/m-agent-readable-repository-refactor/09_adapter_edge_command_tests/complete.log를 확인한다.
|
||||
4. 디렉터리 이름에 없는 추가 predecessor는 요구하지 않는다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/control-plane/cmd/control-plane/main_test.go | TEST-1 |
|
||||
| apps/control-plane/cmd/control-plane/main_test_support_test.go | TEST-1 |
|
||||
| apps/control-plane/cmd/control-plane/config_test.go | TEST-1 |
|
||||
| apps/control-plane/cmd/control-plane/edge_registry_handler_test.go | TEST-1 |
|
||||
| apps/control-plane/cmd/control-plane/fleet_handler_test.go | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/iop-go-build-cache go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/adapters/cli ./apps/edge/cmd/edge ./apps/edge/internal/configrefresh ./apps/control-plane/cmd/control-plane
|
||||
[ "$(GOCACHE=/tmp/iop-go-build-cache go test -list '^Test' ./apps/control-plane/cmd/control-plane | sed '/^ok[[:space:]]/d' | wc -l)" -eq 24 ]
|
||||
python3 -c "from pathlib import Path; roots=[Path('apps/node/internal/adapters'),Path('apps/edge/cmd/edge'),Path('apps/edge/internal/configrefresh'),Path('apps/control-plane/cmd/control-plane')]; fs=sorted(f for r in roots for f in r.rglob('*_test.go')); bad=[(sum(1 for _ in f.open()),str(f)) for f in fs if sum(1 for _ in f.open())>1000]; print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: 다섯 package fresh test PASS, Control Plane Test 24개, 대상 전체에 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/11_client_tests plan=0 tag=TEST -->
|
||||
|
||||
# Code Review Reference - TEST
|
||||
|
||||
> **[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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/11_client_tests, plan=0, tag=TEST
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `client-tests`: Flutter widget 테스트를 app shell과 panel/integration 시나리오별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/11_client_tests/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [TEST-1] client widget harness와 시나리오 파일 분리 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] widget_test.dart를 app shell, panel, provider DTO, notification 시나리오로 옮기고 testWidgets 20개와 make client-test 38 PASS를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 기존 20개 testWidgets 이름/pump/assertion과 전체 38 tests가 보존되었는가.
|
||||
- support harness가 panel별 의도를 숨기지 않는가.
|
||||
- 2분 무진행 정책과 1,000줄 gate가 지켜졌는가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### TEST-1 중간 검증
|
||||
```
|
||||
$ dart format apps/client/test
|
||||
$ test ! -e apps/client/test/widget_test.dart
|
||||
$ [ "$(rg --sort path -n 'testWidgets\(' apps/client/test | wc -l)" -eq 20 ]
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ make client-test
|
||||
$ [ "$(rg --sort path -n 'testWidgets\(' apps/client/test | wc -l)" -eq 20 ]
|
||||
$ python3 -c "from pathlib import Path; fs=sorted(Path('apps/client/test').rglob('*.dart')); bad=[(sum(1 for _ in f.open()),str(f)) for f in fs if sum(1 for _ in f.open())>1000]; print(bad); raise SystemExit(bool(bad))"
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/11_client_tests plan=0 tag=TEST -->
|
||||
|
||||
# Flutter client 테스트 panel/integration 분해
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
apps/client/test/widget_test.dart는 1,596줄에 wire/status fake와 app shell, Edge/Node/runtime/provider/notification 시나리오를 함께 둔다. 공통 harness를 작은 support로 옮기고 panel/integration별 test 파일로 나눈다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `client-tests`: Flutter widget 테스트를 app shell과 panel/integration 시나리오별 파일로 분해
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/client-smoke.md
|
||||
- apps/client/pubspec.yaml
|
||||
- apps/client/test/widget_test.dart
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 리팩터링이라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 client-smoke profile을 읽었다. /sdk/flutter/bin/flutter를 확인했고 현재 checkout에서 make client-test가 38 tests PASS했다. mobile/UI 검증이 2분 무진행이면 blind retry를 중단하고 focused flutter test 출력과 capture 불가 사유를 기록한다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
동작 변경은 없다. widget_test.dart의 20개 testWidgets와 전체 client 38 tests를 그대로 보존한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
운영 심볼의 rename/remove는 없다. 기존 테스트 이름과 package 경계를 유지한다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
Flutter runner와 UI hang risk가 Go 작업과 달라 독립 11_client_tests로 격리한다. 이 plan이 client-tests를 직접 check-on-pass 한다. 최종 readability plan은 이 complete.log를 기다린다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
widget_test.dart와 test-only support/new scenario files만 수정한다. lib UI/DTO, generated Dart, golden/screenshot 추가는 제외한다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 1,596줄과 큰 fake를 6개 시나리오로 나누며 async pump/Completer 의미를 보존해야 한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [TEST-1] widget_test.dart를 app shell, panel, provider DTO, notification 시나리오로 옮기고 testWidgets 20개와 make client-test 38 PASS를 보존한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [TEST-1] client widget harness와 시나리오 파일 분리
|
||||
|
||||
- 문제: apps/client/test/widget_test.dart:14의 FakeWebSocket, 65의 status repository, 422 이후 20개 widget 시나리오가 한 파일에 결합되어 있다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
widget_test.dart:14 class FakeWebSocket implements WebSocket
|
||||
widget_test.dart:65 class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository
|
||||
widget_test.dart:422 testWidgets('Client App basic rendering...')
|
||||
|
||||
After
|
||||
test/support/client_test_harness.dart
|
||||
test/app_shell_test.dart
|
||||
test/edge_nodes_panels_test.dart
|
||||
test/runtime_panel_test.dart
|
||||
test/provider_status_test.dart
|
||||
test/notification_integration_test.dart
|
||||
widget_test.dart removed
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] apps/client/test/widget_test.dart: 모든 scenario를 이동한 뒤 삭제한다.
|
||||
- [ ] apps/client/test/support/client_test_harness.dart: fake wire/status repository와 pump helper를 최소 공개 범위로 둔다.
|
||||
- [ ] apps/client/test/app_shell_test.dart: handshake/error/agent shell/mobile layout을 둔다.
|
||||
- [ ] apps/client/test/edge_nodes_panels_test.dart: Edge/Node/log/refresh를 둔다.
|
||||
- [ ] apps/client/test/runtime_panel_test.dart: domain agent/command/gating을 둔다.
|
||||
- [ ] apps/client/test/provider_status_test.dart: provider rendering/DTO parsing을 둔다.
|
||||
- [ ] apps/client/test/notification_integration_test.dart: Nexo stream/SnackBar를 둔다.
|
||||
- 테스트 작성: 추가 테스트는 작성하지 않는다. 기존 20개 testWidgets의 이름, pump 순서, assertion과 전체 38 tests를 보존한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
dart format apps/client/test
|
||||
test ! -e apps/client/test/widget_test.dart
|
||||
[ "$(rg --sort path -n 'testWidgets\(' apps/client/test | wc -l)" -eq 20 ]
|
||||
~~~
|
||||
|
||||
기대 결과: Dart format 성공, 원본 삭제, testWidgets 20개.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| apps/client/test/widget_test.dart | TEST-1 |
|
||||
| apps/client/test/support/client_test_harness.dart | TEST-1 |
|
||||
| apps/client/test/app_shell_test.dart | TEST-1 |
|
||||
| apps/client/test/edge_nodes_panels_test.dart | TEST-1 |
|
||||
| apps/client/test/runtime_panel_test.dart | TEST-1 |
|
||||
| apps/client/test/provider_status_test.dart | TEST-1 |
|
||||
| apps/client/test/notification_integration_test.dart | TEST-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
make client-test
|
||||
[ "$(rg --sort path -n 'testWidgets\(' apps/client/test | wc -l)" -eq 20 ]
|
||||
python3 -c "from pathlib import Path; fs=sorted(Path('apps/client/test').rglob('*.dart')); bad=[(sum(1 for _ in f.open()),str(f)) for f in fs if sum(1 for _ in f.open())>1000]; print(bad); raise SystemExit(bool(bad))"
|
||||
~~~
|
||||
|
||||
기대 결과: Flutter 전체 38 tests PASS, testWidgets 20개, 1,000줄 초과 test 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/12+03,06,10,11_readability_baseline plan=0 tag=REFACTOR -->
|
||||
|
||||
# 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.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Environment/secret/service blockers, generic scope changes, repeated failures, and evidence gaps that a follow-up agent can close are normal follow-up issues, not user-review blockers by themselves.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record only Milestone lock decisions in `사용자 리뷰 요청` and stop for code-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-07-16
|
||||
task=m-agent-readable-repository-refactor/12+03,06,10,11_readability_baseline, plan=0, tag=REFACTOR
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `readability-baseline`: 추적 소스의 파일/함수/read-set 기준선과 재현 가능한 ratchet audit 도입
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
|
||||
|
||||
1. 판정을 append한다.
|
||||
2. `CODE_REVIEW-local-G07.md` → `code_review_local_G07_N.log`, `PLAN-local-G07.md` → `plan_local_G07_M.log`로 아카이브한다.
|
||||
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-agent-readable-repository-refactor/12+03,06,10,11_readability_baseline/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 연결된 Milestone 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REFACTOR-1] readability audit와 baseline schema 구현 | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [REFACTOR-1] 파일/함수/read-set 기준선과 사유 필수 ratchet을 구현하고 unit test, 반복 JSON cmp, Make audit를 모두 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
|
||||
- [ ] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
|
||||
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
|
||||
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
|
||||
- [ ] PASS이면 active task 디렉터리 `agent-task/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
|
||||
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
|
||||
- [ ] USER_REVIEW가 연결된 Milestone 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- 03,06,10,11 predecessor complete.log가 모두 존재했는가.
|
||||
- tracked input/exclusion과 모든 threshold가 Milestone 정의와 일치하는가.
|
||||
- 신규/증가 초과가 실패하고 기존 예외마다 non-empty reason이 있는가.
|
||||
- 동일 입력 JSON이 byte-identical하고 read-set 합계가 결정적인가.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
|
||||
|
||||
필수 규칙:
|
||||
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
|
||||
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
|
||||
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
|
||||
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
|
||||
|
||||
### REFACTOR-1 중간 검증
|
||||
```
|
||||
$ python3 -m unittest scripts/readability_audit_test.py
|
||||
$ python3 scripts/readability_audit.py --check --output /tmp/iop-readability-a.json
|
||||
$ python3 scripts/readability_audit.py --check --output /tmp/iop-readability-b.json
|
||||
$ cmp /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```
|
||||
$ python3 -m unittest scripts/readability_audit_test.py
|
||||
$ rm -f /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
$ python3 scripts/readability_audit.py --check --output /tmp/iop-readability-a.json
|
||||
$ python3 scripts/readability_audit.py --check --output /tmp/iop-readability-b.json
|
||||
$ cmp /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
$ make readability-audit
|
||||
$ git diff --check
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[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.
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Agent UI Completion | Mixed | Present only for plan-required agent-ui code work; implementing agent fills actual evidence, review agent applies `구현됨` status/evidence update on PASS and copies the section into `complete.log` |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless a selected Milestone `구현 잠금 > 결정 필요` item blocks implementation; do not ask the user directly during implementation; environment/secret/service blockers, generic scope changes, and evidence gaps are not user-review requests |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<!-- task=m-agent-readable-repository-refactor/12+03,06,10,11_readability_baseline plan=0 tag=REFACTOR -->
|
||||
|
||||
# Repository readability audit 기준선과 ratchet
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
> 구현 후 계획의 검증 명령을 그대로 실행하고 CODE_REVIEW의 구현 에이전트 소유 섹션에 실제 메모와 stdout/stderr를 채운 뒤 active 파일을 유지한 채 리뷰 준비 완료를 보고한다. 종결 판정, log rename, complete.log와 archive 이동은 code-review 전용이다. 선택된 Milestone의 미해결 구현 잠금 결정이 실제 구현을 차단할 때만 review stub의 사용자 리뷰 요청에 연결 근거를 기록하고 중단한다. 직접 사용자 질문, chat 선택지, request_user_input, USER_REVIEW.md/complete.log 작성은 금지한다. 환경·secret·service, 일반 범위 조정, 후속으로 보완 가능한 증거 공백은 사용자 리뷰 사유가 아니다.
|
||||
|
||||
## 배경
|
||||
|
||||
테스트 분해가 끝난 구조를 기준으로 사람이 관리하는 추적 소스의 파일 LOC, 함수 크기, task-local read set을 결정적으로 측정해야 한다. 명시적 사유가 있는 기존 초과만 baseline에 고정하고 신규 초과 또는 수치 증가를 실패시키는 audit와 Make target을 추가한다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
사용자 리뷰 요청은 선택된 Milestone의 구현 잠금 결정만 active review stub의 사용자 리뷰 요청 섹션에 agent-ops/skills/common/_templates/implementation-user-review-request-section.md 형식으로 기록한다. 구현 중 직접 사용자 prompt는 금지하며, 타당성 검증과 USER_REVIEW.md 작성은 code-review가 소유한다.
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md`
|
||||
- Milestone link: [Milestone 문서](agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md)
|
||||
- Task ids:
|
||||
- `readability-baseline`: 추적 소스의 파일/함수/read-set 기준선과 재현 가능한 ratchet audit 도입
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- agent-roadmap/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md
|
||||
- agent-test/local/rules.md
|
||||
- agent-test/local/profiles/testing-smoke.md
|
||||
- Makefile
|
||||
- .gitignore
|
||||
|
||||
### SDD 기준
|
||||
|
||||
선택 Milestone은 SDD: 불필요다. 외부 API/proto/config schema와 runtime 책임 경계를 바꾸지 않는 동작 보존형 리팩터링이라는 기록 사유를 적용한다.
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
test_env=local. local rules와 testing-smoke profile을 읽었다. Python은 repository dependency를 추가하지 않고 stdlib unittest를 사용한다. 생성 JSON은 /tmp 두 파일로 반복 실행해 cmp하고, Make target은 build/readability-audit.json을 생성한다. source/test 동작 변경이 없어 full-cycle/E2E는 필요하지 않다.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
새 audit 로직은 테스트가 없다. tracked input/exclusion 정렬, 파일/함수 threshold, same-value allowlist PASS, 신규/증가 FAIL, read-set 합산과 deterministic JSON을 scripts/readability_audit_test.py에 작성한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
새 Make target readability-audit와 새 Python CLI만 추가한다. 기존 심볼 rename/remove와 call site는 없다.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
이 디렉터리의 +03,06,10,11은 openai-tests, core-tests, adapter-command-tests, client-tests 최종 sibling의 complete.log를 모두 요구한다. 테스트 분해 뒤의 구조를 baseline으로 고정하기 위한 실제 의존성이고 추가 predecessor는 없다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
Git이 추적하는 Go/Dart/Shell/Python/Kotlin/Swift만 입력으로 삼고 proto/gen/**, apps/client/lib/gen/**, build/vendor/data-only는 제외한다. production 동작, CI workflow, agent-ops Skill 내용은 수정하지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
local-G07: 새 deterministic audit/ratchet CLI, 다언어 함수 계측, baseline schema와 unit/boundary test가 필요하지만 외부 dependency 없이 국소 검증 가능하다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [REFACTOR-1] 파일/함수/read-set 기준선과 사유 필수 ratchet을 구현하고 unit test, 반복 JSON cmp, Make audit를 모두 통과시킨다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REFACTOR-1] readability audit와 baseline schema 구현
|
||||
|
||||
- 문제: Makefile:1의 PHONY와 76~77의 test target에는 가독성 audit 진입점이 없고 scripts/에도 추적 파일 LOC/함수/read-set의 결정적 ratchet 도구가 없다.
|
||||
- 해결 방법:
|
||||
|
||||
~~~text
|
||||
Before
|
||||
Makefile:1 .PHONY: ... test ... clean
|
||||
Makefile:76 test:
|
||||
Makefile:77 go test ./...
|
||||
|
||||
After
|
||||
Makefile readability-audit target
|
||||
scripts/readability_audit.py
|
||||
scripts/readability_baseline.json
|
||||
scripts/readability_read_sets.json
|
||||
scripts/readability_audit_test.py
|
||||
build/readability-audit.json (ignored output)
|
||||
~~~
|
||||
|
||||
- 수정 파일 및 체크리스트:
|
||||
- [ ] Makefile: .PHONY와 readability-audit target을 추가한다.
|
||||
- [ ] scripts/readability_audit.py: git ls-files -z 입력, exclusion, LOC/함수/read-set 측정, sorted JSON, ratchet check를 구현한다.
|
||||
- [ ] scripts/readability_baseline.json: threshold 초과마다 path/metric/value/non-empty reason을 기록한다.
|
||||
- [ ] scripts/readability_read_sets.json: 대표 task별 ordered path와 budget/예외 사유를 기록한다.
|
||||
- [ ] scripts/readability_audit_test.py: deterministic/exclusion/threshold/ratchet/read-set 정상·경계 테스트를 작성한다.
|
||||
- 테스트 작성: 새 기능이므로 테스트를 작성한다. scripts/readability_audit_test.py의 test_tracked_inputs_are_sorted_and_generated_paths_excluded, test_threshold_boundaries, test_allowlist_same_value_passes, test_new_or_increased_violation_fails, test_read_set_totals_are_deterministic가 fixture tree와 임시 baseline을 사용해 각각 입력/경계/ratchet/read-set을 assert한다.
|
||||
- 중간 검증:
|
||||
|
||||
~~~bash
|
||||
python3 -m unittest scripts/readability_audit_test.py
|
||||
python3 scripts/readability_audit.py --check --output /tmp/iop-readability-a.json
|
||||
python3 scripts/readability_audit.py --check --output /tmp/iop-readability-b.json
|
||||
cmp /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
~~~
|
||||
|
||||
기대 결과: unit tests PASS, 두 audit JSON byte-identical, 신규/증가 초과 없음.
|
||||
|
||||
## 의존 관계 및 구현 순서
|
||||
|
||||
1. agent-task/m-agent-readable-repository-refactor/03+02_openai_provider_tools/complete.log를 확인한다.
|
||||
2. agent-task/m-agent-readable-repository-refactor/06+04,05_core_edge_tests/complete.log를 확인한다.
|
||||
3. agent-task/m-agent-readable-repository-refactor/10+07,08,09_adapter_control_plane_tests/complete.log를 확인한다.
|
||||
4. agent-task/m-agent-readable-repository-refactor/11_client_tests/complete.log를 확인한다.
|
||||
5. 디렉터리 이름에 없는 추가 predecessor는 요구하지 않는다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|---|---|
|
||||
| Makefile | REFACTOR-1 |
|
||||
| scripts/readability_audit.py | REFACTOR-1 |
|
||||
| scripts/readability_baseline.json | REFACTOR-1 |
|
||||
| scripts/readability_read_sets.json | REFACTOR-1 |
|
||||
| scripts/readability_audit_test.py | REFACTOR-1 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
~~~bash
|
||||
python3 -m unittest scripts/readability_audit_test.py
|
||||
rm -f /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
python3 scripts/readability_audit.py --check --output /tmp/iop-readability-a.json
|
||||
python3 scripts/readability_audit.py --check --output /tmp/iop-readability-b.json
|
||||
cmp /tmp/iop-readability-a.json /tmp/iop-readability-b.json
|
||||
make readability-audit
|
||||
git diff --check
|
||||
~~~
|
||||
|
||||
기대 결과: unit tests PASS, audit 결과 재현 가능, 신규/증가 초과는 0, baseline 예외마다 사유 존재, Make target PASS.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
|
||||
1070
apps/edge/internal/openai/chat_handler_test.go
Normal file
1070
apps/edge/internal/openai/chat_handler_test.go
Normal file
File diff suppressed because it is too large
Load diff
848
apps/edge/internal/openai/chat_stream_reasoning_test.go
Normal file
848
apps/edge/internal/openai/chat_stream_reasoning_test.go
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/packages/go/config"
|
||||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"from-request",
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"max_completion_tokens":7
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
options, ok := fake.req.Input["options"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("options not passed: %+v", fake.req.Input)
|
||||
}
|
||||
if options["max_tokens"].(int) != 7 {
|
||||
t.Fatalf("max_completion_tokens not mapped to provider max_tokens: %+v", options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamsSSE(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"finish_reason":"length"`) {
|
||||
t.Fatalf("streaming finish_reason did not preserve provider value:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamSynthesizesToolCallsFromClineTextBlock(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call>\n<function=run_commands>\n<parameter=commands>\n[\"cd /config/workspace/iop && git status\"]\n</parameter>\n</function>\n</tool_call>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
|
||||
t.Fatalf("stream did not synthesize tool_calls:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "<tool_call>") || strings.Contains(body, `\u003ctool_call`) {
|
||||
t.Fatalf("stream leaked raw tool_call block:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"index":0`) {
|
||||
t.Fatalf("stream tool_call delta should include index:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) {
|
||||
t.Fatalf("stream should preserve text before tool call:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamWithToolsBuffersTextUntilComplete(t *testing.T) {
|
||||
fake := &fakeRunService{events: bufferedRunEvents(
|
||||
&iop.RunEvent{Type: "delta", Delta: "streaming before complete"},
|
||||
&iop.RunEvent{Type: "complete"},
|
||||
)}
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"explain sudo"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
reqs := fake.reqsSnapshot()
|
||||
if len(reqs) != 1 {
|
||||
t.Fatalf("submit attempts: got %d want 1", len(reqs))
|
||||
}
|
||||
if reqs[0].Metadata[runtimeMetadataToolValidationAttempt] != "1" {
|
||||
t.Fatalf("tool stream should enable buffered validation metadata: %+v", reqs[0].Metadata)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"content":"streaming before complete"`) ||
|
||||
!strings.Contains(body, `"finish_reason":"stop"`) ||
|
||||
!strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("buffered stream did not emit final content:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamSynthesizesToolCallsFromClineTemplateCall(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "먼저 현재 git 상태를 확인하겠습니다.\n\n"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "{{run_commands(commands=[{'command': 'git status', 'description': '현재 git 상태 확인', 'runInTerminal': True}]})}}"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"finish_reason":"tool_calls"`) {
|
||||
t.Fatalf("stream did not synthesize tool_calls:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "{{run_commands") {
|
||||
t.Fatalf("stream leaked raw template tool call:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"index":0`) {
|
||||
t.Fatalf("stream tool_call delta should include index:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `\"commands\":[{\"args\":[\"status\"],\"command\":\"git\"}]`) &&
|
||||
!strings.Contains(body, `\"commands\":[{\"command\":\"git\",\"args\":[\"status\"]}]`) {
|
||||
t.Fatalf("stream tool_call arguments should include command:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "runInTerminal") || strings.Contains(body, "description") {
|
||||
t.Fatalf("stream tool_call arguments should strip execution metadata:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"먼저 현재 git 상태를 확인하겠습니다."`) {
|
||||
t.Fatalf("stream should preserve text before tool call:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamPassesThroughNativeToolCallsFromProviderRoute(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "checking"}
|
||||
fake.events <- &iop.RunEvent{
|
||||
Type: "complete",
|
||||
Metadata: map[string]string{
|
||||
"finish_reason": "tool_calls",
|
||||
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
|
||||
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"finish_reason":"tool_calls"`) {
|
||||
t.Fatalf("stream finish_reason:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamDropsRawTextWhenNativeToolCallsArrive(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: `<tool_call><function=run_commands><parameter=commands>["git status"]</parameter></function></tool_call>`}
|
||||
fake.events <- &iop.RunEvent{
|
||||
Type: "complete",
|
||||
Metadata: map[string]string{
|
||||
"finish_reason": "tool_calls",
|
||||
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
|
||||
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
|
||||
t.Fatalf("stream leaked raw text tool call:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsToolsStreamNativeToolCallPreservesLeadingProse(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "선행 prose\n\n<tool_call><function=run_commands><parameter=commands>[\"git status\"]</parameter></function></tool_call>"}
|
||||
fake.events <- &iop.RunEvent{
|
||||
Type: "complete",
|
||||
Metadata: map[string]string{
|
||||
"finish_reason": "tool_calls",
|
||||
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"content":"선행 prose"`) {
|
||||
t.Fatalf("stream dropped leading prose:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
|
||||
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
|
||||
}
|
||||
if strings.Contains(body, "<tool_call") || strings.Contains(body, `\u003ctool_call`) {
|
||||
t.Fatalf("stream leaked raw text tool call:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamDropsPartialTextToolCallSuffixWhenNativeToolCallsArrive(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
delta string
|
||||
wantContent string
|
||||
leakMarkers []string
|
||||
}{
|
||||
{
|
||||
name: "xml partial candidate suffix",
|
||||
delta: "checking status <tool_ca",
|
||||
wantContent: `"content":"checking status"`,
|
||||
leakMarkers: []string{`\u003ctool_ca`},
|
||||
},
|
||||
{
|
||||
name: "mustache partial candidate suffix",
|
||||
delta: "checking status {",
|
||||
wantContent: `"content":"checking status"`,
|
||||
leakMarkers: []string{`"content":"{"`},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: tc.delta}
|
||||
fake.events <- &iop.RunEvent{
|
||||
Type: "complete",
|
||||
Metadata: map[string]string{
|
||||
"finish_reason": "tool_calls",
|
||||
runtimeMetadataOpenAIToolCalls: `[{"id":"call_1","type":"function","function":{"name":"run_commands","arguments":"{\"commands\":[\"git status\"]}"}}]`,
|
||||
},
|
||||
}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}],
|
||||
"tool_choice":"auto"
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"tool_calls"`) || !strings.Contains(body, `"call_1"`) {
|
||||
t.Fatalf("stream did not pass through native tool_calls:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"finish_reason":"tool_calls"`) {
|
||||
t.Fatalf("stream finish_reason:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, tc.wantContent) {
|
||||
t.Fatalf("stream dropped safe prefix content %s:\n%s", tc.wantContent, body)
|
||||
}
|
||||
for _, marker := range tc.leakMarkers {
|
||||
if strings.Contains(body, marker) {
|
||||
t.Fatalf("stream leaked partial text tool call marker %q:\n%s", marker, body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamingReportsClosedRunStream(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent)}
|
||||
close(fake.events)
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"message":"run stream closed"`) || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE error body:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsReturnsReasoningContentSeparately(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "more"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{
|
||||
Adapter: "ollama",
|
||||
Target: "llama-fixed",
|
||||
}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"client-model",
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Choices[0].Message.Content != "answer" {
|
||||
t.Fatalf("content: got %q, expected \"answer\"", resp.Choices[0].Message.Content)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "thinking more" {
|
||||
t.Fatalf("reasoning_content: got %q, expected \"thinking more\"", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsFallsBackToReasoningWhenContentEmpty(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "only"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"client-model",
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
content := resp.Choices[0].Message.Content
|
||||
if !strings.Contains(content, "thinking only") || !strings.Contains(content, "finish_reason=length") {
|
||||
t.Fatalf("content fallback did not preserve reasoning and finish reason: %q", content)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "thinking only" {
|
||||
t.Fatalf("reasoning_content: got %q, expected \"thinking only\"", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsSuppressesReasoningContentWhenExcluded(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "more"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "answer"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Usage: &iop.Usage{InputTokens: 2, OutputTokens: 2}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{
|
||||
Adapter: "ollama",
|
||||
Target: "llama-fixed",
|
||||
}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"client-model",
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"include_reasoning": false
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Choices[0].Message.Content != "answer" {
|
||||
t.Fatalf("content: got %q, expected \"answer\"", resp.Choices[0].Message.Content)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "" {
|
||||
t.Fatalf("reasoning_content: got %q, expected empty string due to include_reasoning=false", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsReportsHiddenReasoningWhenExcludedAndContentEmpty(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "thinking "}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "only"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"client-model",
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"include_reasoning": false
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
content := resp.Choices[0].Message.Content
|
||||
if content == "" {
|
||||
t.Fatalf("content should report hidden reasoning instead of returning empty success")
|
||||
}
|
||||
if strings.Contains(content, "thinking only") {
|
||||
t.Fatalf("hidden reasoning fallback leaked reasoning content: %q", content)
|
||||
}
|
||||
if !strings.Contains(content, "reasoning was hidden by include_reasoning=false") || !strings.Contains(content, "finish_reason=length") {
|
||||
t.Fatalf("hidden reasoning fallback did not report cause and finish reason: %q", content)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "" {
|
||||
t.Fatalf("reasoning_content: got %q, expected empty string due to include_reasoning=false", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamsReasoningSSE(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"reasoning_content":"think"`) || !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputStreamsExplicitReasoningSSE(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion><result>hi</result></attempt_completion>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"include_reasoning": true
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"reasoning_content":"think"`) ||
|
||||
!strings.Contains(body, `"content":"\u003cattempt_completion\u003e\u003cresult\u003ehi\u003c/result\u003e\u003c/attempt_completion\u003e"`) ||
|
||||
!strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamFallsBackToReasoningWhenContentEmpty(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think only"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"reasoning_content":"think only"`) ||
|
||||
!strings.Contains(body, `"content":"think only`) ||
|
||||
!strings.Contains(body, "finish_reason=length") ||
|
||||
!strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamSuppressesReasoningWhenExcluded(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hi"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"include_reasoning": false
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "reasoning_content") || strings.Contains(body, "think") {
|
||||
t.Fatalf("unexpected reasoning_content in SSE body when excluded:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"hi"`) || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body, missing content or DONE:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamSanitizesKnownSentinelTokenAcrossDeltas(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think <|mask"}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "_end|>"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "hello <|mask"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "_end|> world"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"metadata":{},
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "<|mask") || strings.Contains(body, "mask_end") {
|
||||
t.Fatalf("stream leaked sentinel token:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"content":"hello "`) || !strings.Contains(body, `"content":"world"`) {
|
||||
t.Fatalf("stream did not preserve sanitized content chunks:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"reasoning_content":"think "`) || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("stream did not preserve sanitized reasoning or done marker:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStreamReportsHiddenReasoningWhenExcludedAndContentEmpty(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "think only"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete", Metadata: map[string]string{"finish_reason": "length"}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"stream-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"hi"}],
|
||||
"include_reasoning": false
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "reasoning_content") || strings.Contains(body, "think only") {
|
||||
t.Fatalf("unexpected reasoning fallback in SSE body when excluded:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, "reasoning was hidden by include_reasoning=false") ||
|
||||
!strings.Contains(body, "finish_reason=length") ||
|
||||
!strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected SSE body, missing hidden reasoning report or DONE:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputNormalizesXMLStyleAgentResponse(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<thinking>hidden</thinking>\n\n<attempt_completion>\n<result>ok</result>\n</attempt completion>\n```\ntrailing noise"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"agent-model",
|
||||
"messages":[{"role":"user","content":"finish"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if fake.req.Input["think"] != false {
|
||||
t.Fatalf("strict output should disable thinking by default: %+v", fake.req.Input)
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
want := "<attempt_completion>\n<result>ok</result>\n</attempt_completion>"
|
||||
if resp.Choices[0].Message.Content != want {
|
||||
t.Fatalf("content:\ngot %q\nwant %q", resp.Choices[0].Message.Content, want)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "" {
|
||||
t.Fatalf("strict output should drop reasoning content: %q", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputPreservesExplicitReasoningContent(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden thought"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion>\n<result>ok</result>\n</attempt_completion>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"agent-model",
|
||||
"messages":[{"role":"user","content":"finish"}],
|
||||
"include_reasoning": true
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Choices[0].Message.Content != "<attempt_completion>\n<result>ok</result>\n</attempt_completion>" {
|
||||
t.Fatalf("content: %q", resp.Choices[0].Message.Content)
|
||||
}
|
||||
if resp.Choices[0].Message.ReasoningContent != "hidden thought" {
|
||||
t.Fatalf("reasoning_content: got %q, want hidden thought", resp.Choices[0].Message.ReasoningContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputWrapsPlainTextWhenPromptDeclaresCompletionTool(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 3)}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "plain answer"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"agent-model",
|
||||
"messages":[
|
||||
{"role":"system","content":"Once you've completed the user's task, you must use the attempt_completion tool to present the result.\n\n<attempt_completion>\n<result>done</result>\n</attempt_completion>"},
|
||||
{"role":"user","content":"finish"}
|
||||
]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(fake.req.Prompt, "output exactly one <attempt_completion> block") || !strings.Contains(fake.req.Prompt, "actual user-facing response") {
|
||||
t.Fatalf("strict contract instruction not injected:\n%s", fake.req.Prompt)
|
||||
}
|
||||
var resp chatCompletionResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
want := "<attempt_completion>\n<result>plain answer</result>\n</attempt_completion>"
|
||||
if resp.Choices[0].Message.Content != want {
|
||||
t.Fatalf("content:\ngot %q\nwant %q", resp.Choices[0].Message.Content, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputBuffersStreamingResponse(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 4)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "---\n<think>noise</think><read_file><path>a.go</path></read file>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true, StrictStreamBuffer: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"agent-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"read"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "reasoning_content") || strings.Contains(body, "<think>") || strings.Contains(body, "</read file>") {
|
||||
t.Fatalf("strict stream leaked unstable content:\n%s", body)
|
||||
}
|
||||
if !strings.Contains(body, "read_file") || !strings.Contains(body, "a.go") || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("unexpected strict SSE body:\n%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsStrictOutputStreamsLiveContent(t *testing.T) {
|
||||
fake := &fakeRunService{events: make(chan *iop.RunEvent, 5)}
|
||||
fake.events <- &iop.RunEvent{Type: "reasoning_delta", Delta: "hidden"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<attempt_completion>\n"}
|
||||
fake.events <- &iop.RunEvent{Type: "delta", Delta: "<result>ok</result>\n</attempt_completion>"}
|
||||
fake.events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", StrictOutput: true}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"agent-model",
|
||||
"stream":true,
|
||||
"messages":[{"role":"user","content":"finish"}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "reasoning_content") || strings.Contains(body, "hidden") {
|
||||
t.Fatalf("strict stream leaked reasoning content:\n%s", body)
|
||||
}
|
||||
if strings.Count(body, `"content":`) != 2 || !strings.Contains(body, "attempt_completion") || !strings.Contains(body, "data: [DONE]") {
|
||||
t.Fatalf("strict stream should pass live content deltas:\n%s", body)
|
||||
}
|
||||
}
|
||||
480
apps/edge/internal/openai/chat_tool_synthesis_test.go
Normal file
480
apps/edge/internal/openai/chat_tool_synthesis_test.go
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"iop/packages/go/config"
|
||||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
func TestValidateJSONSchemaSubsetCoversObjectArrayEnumAdditionalProperties(t *testing.T) {
|
||||
schema := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"mode": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []any{"read", "write"},
|
||||
},
|
||||
"commands": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
},
|
||||
"options": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"dry_run": map[string]any{"type": "boolean"},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
},
|
||||
"required": []any{"mode", "commands"},
|
||||
"additionalProperties": false,
|
||||
}
|
||||
|
||||
valid := map[string]any{
|
||||
"mode": "read",
|
||||
"commands": []any{"git status"},
|
||||
"options": map[string]any{"dry_run": true},
|
||||
}
|
||||
if err := validateJSONSchemaSubset(valid, schema, "$.arguments"); err != nil {
|
||||
t.Fatalf("valid schema rejected: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "enum",
|
||||
args: map[string]any{"mode": "delete", "commands": []any{"git status"}},
|
||||
want: "$.arguments.mode value",
|
||||
},
|
||||
{
|
||||
name: "array item type",
|
||||
args: map[string]any{"mode": "read", "commands": []any{1}},
|
||||
want: "$.arguments.commands[0] type integer does not match schema type string",
|
||||
},
|
||||
{
|
||||
name: "additional properties",
|
||||
args: map[string]any{"mode": "read", "commands": []any{"git status"}, "extra": true},
|
||||
want: "$.arguments.extra is not allowed",
|
||||
},
|
||||
{
|
||||
name: "nested additional properties",
|
||||
args: map[string]any{"mode": "read", "commands": []any{"git status"}, "options": map[string]any{"dry_run": true, "trace": true}},
|
||||
want: "$.arguments.options.trace is not allowed",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateJSONSchemaSubset(tc.args, schema, "$.arguments")
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("error got %v, want substring %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallConvertsCommandObjectsToShellStrings(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'command -v git && echo \"found\" || echo \"not found\"', 'runInTerminal': True}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if cleaned != "확인합니다." {
|
||||
t.Fatalf("content: got %q", cleaned)
|
||||
}
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
commands := args["commands"].([]any)
|
||||
if commands[0] != `command -v git && echo "found" || echo "not found"` {
|
||||
t.Fatalf("commands args: %+v", commands)
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
t.Fatalf("runInTerminal should not be emitted: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallStripsTopLevelExecutionHints(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git status'}], runInTerminal=True, description='status')}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
commands := args["commands"].([]any)
|
||||
command := commands[0].(map[string]any)
|
||||
if command["command"] != "git" {
|
||||
t.Fatalf("commands args: %+v", commands)
|
||||
}
|
||||
commandArgs := command["args"].([]any)
|
||||
if len(commandArgs) != 1 || commandArgs[0] != "status" {
|
||||
t.Fatalf("command args: %+v", commandArgs)
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
t.Fatalf("runInTerminal should not be emitted: %+v", args)
|
||||
}
|
||||
if _, ok := args["description"]; ok {
|
||||
t.Fatalf("description should not be emitted: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallKeepsStructuredCommandArgs(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git', 'args': ['status', '--short'], 'description': 'status'}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
commands := args["commands"].([]any)
|
||||
command := commands[0].(map[string]any)
|
||||
if command["command"] != "git" {
|
||||
t.Fatalf("command: %+v", command)
|
||||
}
|
||||
commandArgs := command["args"].([]any)
|
||||
if len(commandArgs) != 2 || commandArgs[0] != "status" || commandArgs[1] != "--short" {
|
||||
t.Fatalf("command args: %+v", commandArgs)
|
||||
}
|
||||
if _, ok := command["description"]; ok {
|
||||
t.Fatalf("description should not be emitted: %+v", command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallFollowsClineUnionSchemaWithShellStrings(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git', 'args': ['-C', '/config/workspace/iop', 'status']}, {'command': 'pwd && ls -la /config/workspace/'}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"commands": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"anyOf": []any{
|
||||
map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{"type": "string"},
|
||||
"args": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string][]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if len(args["commands"]) != 2 {
|
||||
t.Fatalf("commands len: %+v", args["commands"])
|
||||
}
|
||||
if args["commands"][0] != "git -C /config/workspace/iop status" {
|
||||
t.Fatalf("first command: %+v", args["commands"][0])
|
||||
}
|
||||
if args["commands"][1] != "pwd && ls -la /config/workspace/" {
|
||||
t.Fatalf("second command: %+v", args["commands"][1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallConvertsStructuredShellOperatorsToString(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status', '&&', 'git', 'diff', '--stat']}, {'command': 'ls', 'args': ['-la', '/config/workspace/iop', '&&', 'pwd']}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
commands := args["commands"].([]any)
|
||||
if commands[0] != "cd /config/workspace/iop && git status && git diff --stat" {
|
||||
t.Fatalf("first command: %+v", commands[0])
|
||||
}
|
||||
if commands[1] != "ls -la /config/workspace/iop && pwd" {
|
||||
t.Fatalf("second command: %+v", commands[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallFollowsStringArrayCommandSchema(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git status', 'description': 'status'}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"commands": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string][]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if len(args["commands"]) != 1 || args["commands"][0] != "git status" {
|
||||
t.Fatalf("commands args: %+v", args["commands"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallFollowsSingleCommandSchema(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{bash(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status']}], runInTerminal=True)}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "bash",
|
||||
"parameters": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
_, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if args["command"] != "cd /config/workspace/iop && git status" {
|
||||
t.Fatalf("command arg: %+v", args)
|
||||
}
|
||||
if _, ok := args["commands"]; ok {
|
||||
t.Fatalf("commands should not be emitted: %+v", args)
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
t.Fatalf("runInTerminal should not be emitted: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesShellCommandStringWithoutExtraExecutionHints(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=['command -v git && echo \"found\" || echo \"not found\"'])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if cleaned != "확인합니다." {
|
||||
t.Fatalf("content: got %q", cleaned)
|
||||
}
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call := toolCalls[0].(map[string]any)
|
||||
fn := call["function"].(map[string]any)
|
||||
var args map[string]any
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
commands := args["commands"].([]any)
|
||||
if commands[0] != `command -v git && echo "found" || echo "not found"` {
|
||||
t.Fatalf("commands args: %+v", commands)
|
||||
}
|
||||
if _, ok := args["runInTerminal"]; ok {
|
||||
t.Fatalf("runInTerminal should not be emitted: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizesTemplateToolCallWithClosingMarkerInString(t *testing.T) {
|
||||
content := "확인합니다.\n\n{{run_commands(commands=[{'command': \"printf ')}}'\", 'description': 'marker test', 'runInTerminal': False}])}}"
|
||||
tools := []any{map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "run_commands",
|
||||
},
|
||||
}}
|
||||
|
||||
cleaned, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test")
|
||||
|
||||
if cleaned != "확인합니다." {
|
||||
t.Fatalf("content: got %q", cleaned)
|
||||
}
|
||||
if len(toolCalls) != 1 {
|
||||
t.Fatalf("tool_calls len: got %d", len(toolCalls))
|
||||
}
|
||||
call, ok := toolCalls[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tool call shape: %+v", toolCalls[0])
|
||||
}
|
||||
fn, ok := call["function"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("function shape: %+v", call["function"])
|
||||
}
|
||||
var args map[string][]string
|
||||
if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil {
|
||||
t.Fatalf("arguments JSON: %v", err)
|
||||
}
|
||||
if len(args["commands"]) != 1 || args["commands"][0] != "printf ')}}'" {
|
||||
t.Fatalf("commands args: %+v", args["commands"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsLeavesUnknownTextToolCallAsContent(t *testing.T) {
|
||||
run1Events := make(chan *iop.RunEvent, 2)
|
||||
run1Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
|
||||
run1Events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
run2Events := make(chan *iop.RunEvent, 2)
|
||||
run2Events <- &iop.RunEvent{Type: "delta", Delta: "<tool_call><function=delete_everything><parameter=confirm>true</parameter></function></tool_call>"}
|
||||
run2Events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"qwen3.6:35b",
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp errorResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Error.Type != "tool_validation_error" {
|
||||
t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
|
||||
}
|
||||
if !strings.Contains(resp.Error.Message, "delete_everything") {
|
||||
t.Fatalf("unexpected error message: %q", resp.Error.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatCompletionsLeavesUnknownTemplateToolCallAsContent(t *testing.T) {
|
||||
run1Events := make(chan *iop.RunEvent, 2)
|
||||
run1Events <- &iop.RunEvent{Type: "delta", Delta: "{{delete_everything(confirm=True)}}"}
|
||||
run1Events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
run2Events := make(chan *iop.RunEvent, 2)
|
||||
run2Events <- &iop.RunEvent{Type: "delta", Delta: "{{delete_everything(confirm=True)}}"}
|
||||
run2Events <- &iop.RunEvent{Type: "complete"}
|
||||
|
||||
fake := &fakeRunService{eventRuns: []chan *iop.RunEvent{run1Events, run2Events}}
|
||||
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "cli", Target: "codex"}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{
|
||||
"model":"qwen3.6:35b",
|
||||
"messages":[{"role":"user","content":"status"}],
|
||||
"tools":[{"type":"function","function":{"name":"run_commands"}}]
|
||||
}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleChatCompletions(w, req)
|
||||
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("status: got %d, want 502; body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp errorResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if resp.Error.Type != "tool_validation_error" {
|
||||
t.Fatalf("unexpected error type: got %q, want tool_validation_error", resp.Error.Type)
|
||||
}
|
||||
if !strings.Contains(resp.Error.Message, "delete_everything") {
|
||||
t.Fatalf("unexpected error message: %q", resp.Error.Message)
|
||||
}
|
||||
}
|
||||
169
apps/edge/internal/openai/openai_auth_routes_models_test.go
Normal file
169
apps/edge/internal/openai/openai_auth_routes_models_test.go
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
edgeservice "iop/apps/edge/internal/service"
|
||||
"iop/packages/go/config"
|
||||
)
|
||||
|
||||
func TestRoutesRequireBearerTokenWhenConfigured(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{
|
||||
BearerToken: "secret-token",
|
||||
Models: []string{"model-a"},
|
||||
}, &fakeRunService{}, nil)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
auth string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "missing", wantStatus: http.StatusUnauthorized},
|
||||
{name: "wrong", auth: "Bearer wrong", wantStatus: http.StatusUnauthorized},
|
||||
{name: "valid", auth: "Bearer secret-token", wantStatus: http.StatusOK},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||
if tc.auth != "" {
|
||||
req.Header.Set("Authorization", tc.auth)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.routes().ServeHTTP(w, req)
|
||||
|
||||
if w.Code != tc.wantStatus {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthzDoesNotRequireBearerToken(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{BearerToken: "secret-token"}, &fakeRunService{}, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.routes().ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelsUsesConfiguredModelsOrTarget(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{Target: "fallback-model"}, &fakeRunService{}, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleModels(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "fallback-model") {
|
||||
t.Fatalf("expected target model, got %s", w.Body.String())
|
||||
}
|
||||
|
||||
srv = NewServer(config.EdgeOpenAIConf{Models: []string{"a", "b"}}, &fakeRunService{}, nil)
|
||||
w = httptest.NewRecorder()
|
||||
srv.handleModels(w, req)
|
||||
if !strings.Contains(w.Body.String(), `"id":"a"`) || !strings.Contains(w.Body.String(), `"id":"b"`) {
|
||||
t.Fatalf("expected configured models, got %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIModelsUsesRefreshedCatalog verifies that a provider-pool catalog
|
||||
// applied via SetModelCatalog (config refresh apply) is reflected in /v1/models
|
||||
// on the next request, replacing the previous catalog snapshot.
|
||||
func TestOpenAIModelsUsesRefreshedCatalog(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
|
||||
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-old"}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
|
||||
w := httptest.NewRecorder()
|
||||
srv.handleModels(w, req)
|
||||
if body := w.Body.String(); !strings.Contains(body, `"id":"model-old"`) {
|
||||
t.Fatalf("expected initial catalog model-old, got %s", body)
|
||||
}
|
||||
|
||||
// Refresh the catalog: the next request must show the new model and not the old.
|
||||
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "model-new"}})
|
||||
w = httptest.NewRecorder()
|
||||
srv.handleModels(w, req)
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"id":"model-new"`) {
|
||||
t.Fatalf("expected refreshed catalog model-new, got %s", body)
|
||||
}
|
||||
if strings.Contains(body, `"id":"model-old"`) {
|
||||
t.Fatalf("stale catalog model-old still present after refresh, got %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIProviderPoolRouteUsesRefreshedCatalog verifies that provider-pool
|
||||
// route resolution reads the refreshed catalog snapshot, so a model added by a
|
||||
// config refresh is routed via the provider pool.
|
||||
func TestOpenAIProviderPoolRouteUsesRefreshedCatalog(t *testing.T) {
|
||||
srv := NewServer(config.EdgeOpenAIConf{}, &fakeRunService{}, nil)
|
||||
if srv.findProviderPoolEntry("model-new") != nil {
|
||||
t.Fatal("expected no provider-pool entry before catalog is set")
|
||||
}
|
||||
srv.SetModelCatalog([]config.ModelCatalogEntry{{
|
||||
ID: "model-new",
|
||||
Providers: map[string]string{"prov-a": "served-a"},
|
||||
}})
|
||||
dispatch, ok := srv.resolveRouteDispatch("model-new")
|
||||
if !ok || !dispatch.ProviderPool {
|
||||
t.Fatalf("expected provider-pool dispatch for refreshed model, got ok=%v dispatch=%+v", ok, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaAPIPassthrough(t *testing.T) {
|
||||
fake := &fakeRunService{
|
||||
ollamaResp: edgeservice.OllamaAPIView{
|
||||
StatusCode: http.StatusAccepted,
|
||||
ContentType: "application/json",
|
||||
Body: `{"ok":true}`,
|
||||
},
|
||||
}
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", TimeoutSec: 15}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/show", strings.NewReader(`{"model":"gemma4:26b"}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleOllamaAPI(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if fake.ollamaReq.Adapter != "ollama" || fake.ollamaReq.Method != http.MethodPost || fake.ollamaReq.Path != "/api/show" {
|
||||
t.Fatalf("passthrough req mismatch: %+v", fake.ollamaReq)
|
||||
}
|
||||
if fake.ollamaReq.Body != `{"model":"gemma4:26b"}` || fake.ollamaReq.TimeoutSec != 15 {
|
||||
t.Fatalf("passthrough body/timeout mismatch: %+v", fake.ollamaReq)
|
||||
}
|
||||
if w.Body.String() != `{"ok":true}` {
|
||||
t.Fatalf("body: got %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaAPIPassthroughPreservesConfiguredTarget(t *testing.T) {
|
||||
fake := &fakeRunService{
|
||||
ollamaResp: edgeservice.OllamaAPIView{
|
||||
StatusCode: http.StatusOK,
|
||||
ContentType: "application/json",
|
||||
Body: `{"models":[]}`,
|
||||
},
|
||||
}
|
||||
srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama", Target: "gemma4:26b", TimeoutSec: 15}, fake, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/tags", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
srv.handleOllamaAPI(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status: got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
if fake.ollamaReq.Target != "gemma4:26b" {
|
||||
t.Fatalf("passthrough target: got %q, want gemma4:26b", fake.ollamaReq.Target)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
664
apps/edge/internal/openai/server_test_support_test.go
Normal file
664
apps/edge/internal/openai/server_test_support_test.go
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
edgeservice "iop/apps/edge/internal/service"
|
||||
iop "iop/proto/gen/iop"
|
||||
)
|
||||
|
||||
type fakeRunService struct {
|
||||
req edgeservice.SubmitRunRequest
|
||||
reqs []edgeservice.SubmitRunRequest
|
||||
ollamaReq edgeservice.OllamaAPIRequest
|
||||
ollamaResp edgeservice.OllamaAPIView
|
||||
events chan *iop.RunEvent
|
||||
eventRuns []chan *iop.RunEvent
|
||||
runIDs []string
|
||||
|
||||
submitMu sync.Mutex
|
||||
submitErr error
|
||||
submitErrAfter int // zero means no error; >=1 means error starts from this req index (1-based)
|
||||
cancelMu sync.Mutex
|
||||
cancelCalls []edgeservice.CancelRunRequest
|
||||
cancelErr error
|
||||
|
||||
// tunnelProviderURL, when set, makes SubmitProviderTunnel relay the tunnel
|
||||
// request to this httptest provider fixture and stream the raw provider
|
||||
// response back as ordered frames (emulating the Node relay).
|
||||
tunnelProviderURL string
|
||||
// tunnelServedTarget emulates provider-pool admission: BuildBody is called
|
||||
// with this target when non-empty.
|
||||
tunnelServedTarget string
|
||||
// tunnelFrames, when set, is handed to the tunnel handle as-is instead of
|
||||
// contacting tunnelProviderURL.
|
||||
tunnelFrames chan *iop.ProviderTunnelFrame
|
||||
tunnelErr error
|
||||
tunnelReqs []edgeservice.SubmitProviderTunnelRequest
|
||||
tunnelBodies [][]byte
|
||||
// tunnelWaitTimeout overrides the tunnel handle wait timeout for
|
||||
// timeout-path fixtures; zero keeps the 5s default.
|
||||
tunnelWaitTimeout time.Duration
|
||||
|
||||
// poolDispatchPath records which execution path was returned by
|
||||
// SubmitProviderPool: "provider_tunnel" or "normalized".
|
||||
poolDispatchPath string
|
||||
// poolSubmitErr, when set, is returned by SubmitProviderPool.
|
||||
poolSubmitErr error
|
||||
// poolRunFrames and poolTunnelFrames are the frame sources used by
|
||||
// the normalized path when SubmitProviderPool is in use.
|
||||
poolRunFrames chan *iop.RunEvent
|
||||
poolTunnelFrames chan *iop.ProviderTunnelFrame
|
||||
// lastTunnelHandle stores the tunnel handle from the last SubmitProviderPool
|
||||
// tunnel path call, so tests can check headers set by the handler.
|
||||
lastTunnelHandle *fakeTunnelHandle
|
||||
// poolSubmitResults provides custom results for successive SubmitProviderPool
|
||||
// calls. When non-empty, results are consumed in order; when empty, the
|
||||
// poolDispatchPath / poolSubmitErr fields are used instead.
|
||||
poolSubmitResults []edgeservice.ProviderPoolDispatchResult
|
||||
// poolSubmitCount tracks how many times SubmitProviderPool was called
|
||||
// (including from the chat handler). Used by tests to assert dispatch
|
||||
// surface fidelity.
|
||||
poolSubmitCount int
|
||||
// poolLastRun records the last SubmitRunRequest passed to SubmitProviderPool
|
||||
// (pre-PrepareRun). Tests assert ModelGroupKey is non-empty to catch the
|
||||
// "empty Run" regression.
|
||||
poolLastRun edgeservice.SubmitRunRequest
|
||||
// poolPrepareRunCalled tracks whether the optional PrepareRun hook was
|
||||
// invoked by SubmitProviderPool on the normalized path. Only meaningful
|
||||
// when poolDispatchPath == "normalized".
|
||||
poolPrepareRunCalled bool
|
||||
}
|
||||
|
||||
type fakeTunnelHandle struct {
|
||||
dispatch edgeservice.RunDispatch
|
||||
headers map[string]string
|
||||
frames chan *iop.ProviderTunnelFrame
|
||||
waitTimeout time.Duration
|
||||
}
|
||||
|
||||
func (h *fakeTunnelHandle) Headers() map[string]string { return h.headers }
|
||||
func (h *fakeTunnelHandle) SetHeaders(hdrs map[string]string) { h.headers = hdrs }
|
||||
func (h *fakeTunnelHandle) Dispatch() edgeservice.RunDispatch { return h.dispatch }
|
||||
func (h *fakeTunnelHandle) Stream() edgeservice.ProviderTunnelStream {
|
||||
return edgeservice.ProviderTunnelStream{Frames: h.frames}
|
||||
}
|
||||
func (h *fakeTunnelHandle) Close() {}
|
||||
func (h *fakeTunnelHandle) WaitTimeout() time.Duration {
|
||||
if h.waitTimeout > 0 {
|
||||
return h.waitTimeout
|
||||
}
|
||||
return 5 * time.Second
|
||||
}
|
||||
|
||||
// fakeRunResult implements edgeservice.RunResult for tests.
|
||||
type fakeRunResult struct {
|
||||
dispatch edgeservice.RunDispatch
|
||||
events chan *iop.RunEvent
|
||||
}
|
||||
|
||||
func (r *fakeRunResult) Dispatch() edgeservice.RunDispatch { return r.dispatch }
|
||||
func (r *fakeRunResult) Stream() edgeservice.RunStream {
|
||||
return edgeservice.RunStream{Events: r.events}
|
||||
}
|
||||
func (r *fakeRunResult) Close() {}
|
||||
func (r *fakeRunResult) WaitTimeout() time.Duration {
|
||||
return 5 * time.Second
|
||||
}
|
||||
|
||||
func (s *fakeRunService) SubmitProviderTunnel(_ context.Context, req edgeservice.SubmitProviderTunnelRequest) (edgeservice.ProviderTunnelResult, error) {
|
||||
s.submitMu.Lock()
|
||||
s.tunnelReqs = append(s.tunnelReqs, req)
|
||||
s.submitMu.Unlock()
|
||||
if s.tunnelErr != nil {
|
||||
return nil, s.tunnelErr
|
||||
}
|
||||
target := req.Target
|
||||
if s.tunnelServedTarget != "" {
|
||||
target = s.tunnelServedTarget
|
||||
}
|
||||
body := req.Body
|
||||
if req.BuildBody != nil {
|
||||
built, err := req.BuildBody(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = built
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.tunnelBodies = append(s.tunnelBodies, body)
|
||||
s.submitMu.Unlock()
|
||||
|
||||
frames := s.tunnelFrames
|
||||
if frames == nil && s.tunnelProviderURL != "" {
|
||||
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Method, req.Path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frames = relayed
|
||||
}
|
||||
return &fakeTunnelHandle{
|
||||
dispatch: edgeservice.RunDispatch{
|
||||
RunID: "run-tunnel",
|
||||
NodeID: "node-1",
|
||||
ModelGroupKey: req.ModelGroupKey,
|
||||
Adapter: req.Adapter,
|
||||
Target: target,
|
||||
SessionID: req.SessionID,
|
||||
TimeoutSec: 5,
|
||||
},
|
||||
frames: frames,
|
||||
waitTimeout: s.tunnelWaitTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SubmitProviderPool is a minimal test double for the provider-pool one-shot
|
||||
// dispatch. It records the selected path (tunnel vs normalized) and returns a
|
||||
// stub result so the OpenAI handler can exercise the selection-first flow.
|
||||
// It also tracks whether the optional PrepareRun hook was invoked, so tests
|
||||
// can assert the handler only calls PrepareRun on the normalized path.
|
||||
func (s *fakeRunService) SubmitProviderPool(_ context.Context, req edgeservice.ProviderPoolDispatchRequest) (*edgeservice.ProviderPoolDispatchResult, error) {
|
||||
s.submitMu.Lock()
|
||||
s.poolSubmitCount++
|
||||
s.poolLastRun = req.Run
|
||||
s.poolPrepareRunCalled = false
|
||||
s.submitMu.Unlock()
|
||||
|
||||
// Custom results for successive calls (e.g. tool-validation retry tests).
|
||||
s.submitMu.Lock()
|
||||
if len(s.poolSubmitResults) > 0 {
|
||||
result := s.poolSubmitResults[0]
|
||||
s.poolSubmitResults = s.poolSubmitResults[1:]
|
||||
s.submitMu.Unlock()
|
||||
// Apply pre-dispatch tunnel preparation for tunnel path when custom
|
||||
// results are used.
|
||||
if result.Path == edgeservice.ProviderPoolPathTunnel && req.PrepareTunnel != nil {
|
||||
tunnelReqPrepared, prepErr := req.PrepareTunnel(req.Tunnel)
|
||||
if prepErr != nil {
|
||||
return nil, prepErr
|
||||
}
|
||||
req.Tunnel = tunnelReqPrepared
|
||||
}
|
||||
return s.buildPoolResult(req, result)
|
||||
}
|
||||
err := s.poolSubmitErr
|
||||
tunnelErr := s.tunnelErr
|
||||
path := s.poolDispatchPath
|
||||
s.submitMu.Unlock()
|
||||
|
||||
if err == nil {
|
||||
err = tunnelErr
|
||||
}
|
||||
|
||||
// Apply pre-dispatch tunnel preparation (mirrors real SubmitProviderPool
|
||||
// behavior: PrepareTunnel runs BEFORE buildProviderTunnelRequest and the
|
||||
// Node Send step). On failure, no tunnel request is recorded or sent.
|
||||
tunnelReq := req.Tunnel
|
||||
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
|
||||
if req.PrepareTunnel != nil {
|
||||
tunnelReqPrepared, prepErr := req.PrepareTunnel(req.Tunnel)
|
||||
if prepErr != nil {
|
||||
return nil, prepErr
|
||||
}
|
||||
tunnelReq = tunnelReqPrepared
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = tunnelErr
|
||||
}
|
||||
|
||||
// Record tunnel request for test assertions.
|
||||
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
|
||||
s.submitMu.Lock()
|
||||
s.tunnelReqs = append(s.tunnelReqs, tunnelReq)
|
||||
s.submitMu.Unlock()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
disp := edgeservice.RunDispatch{
|
||||
RunID: "run-pool-tunnel",
|
||||
NodeID: "node-pool",
|
||||
ModelGroupKey: req.Run.ModelGroupKey,
|
||||
Adapter: "selected",
|
||||
Target: "served",
|
||||
ProviderID: "prov-fake",
|
||||
ProviderType: "vllm",
|
||||
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel),
|
||||
QueueReason: "dispatched",
|
||||
}
|
||||
if path == string(edgeservice.ProviderPoolPathNormalized) {
|
||||
disp.RunID = "run-pool-normalized"
|
||||
}
|
||||
|
||||
if path == string(edgeservice.ProviderPoolPathTunnel) || path == "" {
|
||||
frames := s.tunnelFrames
|
||||
if frames == nil && s.tunnelProviderURL != "" {
|
||||
body := req.Tunnel.Body
|
||||
if req.Tunnel.BuildBody != nil {
|
||||
target := req.Tunnel.Target
|
||||
if s.tunnelServedTarget != "" {
|
||||
target = s.tunnelServedTarget
|
||||
}
|
||||
built, err := req.Tunnel.BuildBody(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = built
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.tunnelBodies = append(s.tunnelBodies, body)
|
||||
s.submitMu.Unlock()
|
||||
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Tunnel.Method, req.Tunnel.Path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frames = relayed
|
||||
} else if frames == nil {
|
||||
frames = staticProviderTunnelFrames(`{"ok":true}`)
|
||||
// Record the built tunnel body even when using a static frame source.
|
||||
body := req.Tunnel.Body
|
||||
if req.Tunnel.BuildBody != nil {
|
||||
target := req.Tunnel.Target
|
||||
if s.tunnelServedTarget != "" {
|
||||
target = s.tunnelServedTarget
|
||||
}
|
||||
if built, buildErr := req.Tunnel.BuildBody(target); buildErr == nil {
|
||||
body = built
|
||||
}
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.tunnelBodies = append(s.tunnelBodies, body)
|
||||
s.submitMu.Unlock()
|
||||
} else {
|
||||
body := req.Tunnel.Body
|
||||
if req.Tunnel.BuildBody != nil {
|
||||
target := req.Tunnel.Target
|
||||
if s.tunnelServedTarget != "" {
|
||||
target = s.tunnelServedTarget
|
||||
}
|
||||
if built, err := req.Tunnel.BuildBody(target); err == nil {
|
||||
body = built
|
||||
}
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.tunnelBodies = append(s.tunnelBodies, body)
|
||||
s.submitMu.Unlock()
|
||||
}
|
||||
|
||||
handle := &fakeTunnelHandle{
|
||||
dispatch: edgeservice.RunDispatch{
|
||||
RunID: "run-tunnel",
|
||||
NodeID: "node-1",
|
||||
ModelGroupKey: req.Tunnel.ModelGroupKey,
|
||||
Adapter: req.Tunnel.Adapter,
|
||||
Target: req.Tunnel.Target,
|
||||
SessionID: req.Tunnel.SessionID,
|
||||
TimeoutSec: 5,
|
||||
},
|
||||
headers: req.Tunnel.Headers,
|
||||
frames: frames,
|
||||
}
|
||||
// Store for later inspection by tests.
|
||||
s.submitMu.Lock()
|
||||
s.lastTunnelHandle = handle
|
||||
s.submitMu.Unlock()
|
||||
return &edgeservice.ProviderPoolDispatchResult{
|
||||
Path: edgeservice.ProviderPoolPathTunnel,
|
||||
Tunnel: handle,
|
||||
DispatchInfo: disp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Normalized path (mimics Ollama/CLI behavior).
|
||||
// Invoke the optional PrepareRun hook so tests can assert it only runs
|
||||
// on the normalized path (mirrors real SubmitProviderPool behavior).
|
||||
if req.PrepareRun != nil {
|
||||
s.submitMu.Lock()
|
||||
s.poolPrepareRunCalled = true
|
||||
s.submitMu.Unlock()
|
||||
prepared, prepErr := req.PrepareRun(req.Run)
|
||||
if prepErr != nil {
|
||||
return nil, prepErr
|
||||
}
|
||||
req.Run = prepared
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.reqs = append(s.reqs, req.Run)
|
||||
s.submitMu.Unlock()
|
||||
|
||||
runChan := s.poolRunFrames
|
||||
if runChan == nil {
|
||||
runChan = make(chan *iop.RunEvent, 1)
|
||||
runChan <- &iop.RunEvent{RunId: disp.RunID, Type: "complete"}
|
||||
close(runChan)
|
||||
}
|
||||
return &edgeservice.ProviderPoolDispatchResult{
|
||||
Path: edgeservice.ProviderPoolPathNormalized,
|
||||
Run: &fakeRunResult{
|
||||
dispatch: disp,
|
||||
events: runChan,
|
||||
},
|
||||
DispatchInfo: disp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// buildPoolResult assembles a ProviderPoolDispatchResult from a pre-configured
|
||||
// custom result. It handles tunnel request recording, frame relay, and
|
||||
// normalized path run construction.
|
||||
func (s *fakeRunService) buildPoolResult(req edgeservice.ProviderPoolDispatchRequest, result edgeservice.ProviderPoolDispatchResult) (*edgeservice.ProviderPoolDispatchResult, error) {
|
||||
if result.Path == edgeservice.ProviderPoolPathTunnel || result.Path == "" {
|
||||
s.submitMu.Lock()
|
||||
s.tunnelReqs = append(s.tunnelReqs, req.Tunnel)
|
||||
s.submitMu.Unlock()
|
||||
var frames chan *iop.ProviderTunnelFrame
|
||||
if frames == nil && s.tunnelProviderURL != "" {
|
||||
body := req.Tunnel.Body
|
||||
if req.Tunnel.BuildBody != nil {
|
||||
target := req.Tunnel.Target
|
||||
if s.tunnelServedTarget != "" {
|
||||
target = s.tunnelServedTarget
|
||||
}
|
||||
if built, err := req.Tunnel.BuildBody(target); err == nil {
|
||||
body = built
|
||||
}
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.tunnelBodies = append(s.tunnelBodies, body)
|
||||
s.submitMu.Unlock()
|
||||
relayed, err := relayProviderFixtureFrames(s.tunnelProviderURL, req.Tunnel.Method, req.Tunnel.Path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frames = relayed
|
||||
} else if frames == nil {
|
||||
frames = staticProviderTunnelFrames(`{"ok":true}`)
|
||||
}
|
||||
disp := result.DispatchInfo
|
||||
if disp.RunID == "" {
|
||||
disp.RunID = "run-tunnel"
|
||||
}
|
||||
if disp.NodeID == "" {
|
||||
disp.NodeID = "node-1"
|
||||
}
|
||||
handle := &fakeTunnelHandle{
|
||||
dispatch: disp,
|
||||
headers: req.Tunnel.Headers,
|
||||
frames: frames,
|
||||
waitTimeout: s.tunnelWaitTimeout,
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.lastTunnelHandle = handle
|
||||
s.submitMu.Unlock()
|
||||
return &edgeservice.ProviderPoolDispatchResult{
|
||||
Path: edgeservice.ProviderPoolPathTunnel,
|
||||
Tunnel: handle,
|
||||
DispatchInfo: disp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Normalized path.
|
||||
disp := result.DispatchInfo
|
||||
if disp.RunID == "" {
|
||||
disp.RunID = "run-pool-normalized"
|
||||
}
|
||||
if disp.NodeID == "" {
|
||||
disp.NodeID = "node-pool"
|
||||
}
|
||||
// Invoke the optional PrepareRun hook on the normalized path (mirrors
|
||||
// real SubmitProviderPool behavior).
|
||||
if req.PrepareRun != nil {
|
||||
prepared, prepErr := req.PrepareRun(req.Run)
|
||||
if prepErr != nil {
|
||||
return nil, prepErr
|
||||
}
|
||||
req.Run = prepared
|
||||
}
|
||||
s.submitMu.Lock()
|
||||
s.poolPrepareRunCalled = true
|
||||
s.submitMu.Unlock()
|
||||
s.submitMu.Lock()
|
||||
s.reqs = append(s.reqs, req.Run)
|
||||
s.submitMu.Unlock()
|
||||
// Use the Run from the custom result if provided, otherwise create a
|
||||
// default completing run.
|
||||
if result.Run != nil {
|
||||
return &edgeservice.ProviderPoolDispatchResult{
|
||||
Path: edgeservice.ProviderPoolPathNormalized,
|
||||
Run: result.Run,
|
||||
DispatchInfo: disp,
|
||||
}, nil
|
||||
}
|
||||
runChan := make(chan *iop.RunEvent, 1)
|
||||
runChan <- &iop.RunEvent{RunId: disp.RunID, Type: "complete"}
|
||||
close(runChan)
|
||||
return &edgeservice.ProviderPoolDispatchResult{
|
||||
Path: edgeservice.ProviderPoolPathNormalized,
|
||||
Run: &fakeRunResult{
|
||||
dispatch: disp,
|
||||
events: runChan,
|
||||
},
|
||||
DispatchInfo: disp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// tunnelHandleSnapshot returns the tunnel handle from the last SubmitProviderPool
|
||||
// call, allowing tests to check headers set by the handler.
|
||||
type tunnelHandleSnapshot struct {
|
||||
headers map[string]string
|
||||
runID string
|
||||
nodeID string
|
||||
}
|
||||
|
||||
func (s *fakeRunService) tunnelHandleSnapshot() tunnelHandleSnapshot {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
h := s.lastTunnelHandle
|
||||
if h == nil {
|
||||
return tunnelHandleSnapshot{}
|
||||
}
|
||||
return tunnelHandleSnapshot{
|
||||
headers: h.Headers(),
|
||||
runID: h.Dispatch().RunID,
|
||||
nodeID: h.Dispatch().NodeID,
|
||||
}
|
||||
}
|
||||
|
||||
// poolSubmitCountSnapshot returns the number of times SubmitProviderPool was
|
||||
// called. Tests assert exactly-once dispatch from the handler.
|
||||
func (s *fakeRunService) poolSubmitCountSnapshot() int {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
return s.poolSubmitCount
|
||||
}
|
||||
|
||||
// poolLastRunSnapshot returns a copy of the last SubmitRunRequest passed to
|
||||
// SubmitProviderPool. Tests assert ModelGroupKey is non-empty to catch the
|
||||
// "empty Run" regression.
|
||||
func (s *fakeRunService) poolLastRunSnapshot() edgeservice.SubmitRunRequest {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
return s.poolLastRun
|
||||
}
|
||||
|
||||
// poolPrepareRunCalledSnapshot returns whether the PrepareRun hook was
|
||||
// invoked by the last SubmitProviderPool call. Only meaningful when the last
|
||||
// call was on the normalized path.
|
||||
func (s *fakeRunService) poolPrepareRunCalledSnapshot() bool {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
return s.poolPrepareRunCalled
|
||||
}
|
||||
|
||||
func (s *fakeRunService) tunnelReqsSnapshot() []edgeservice.SubmitProviderTunnelRequest {
|
||||
s.submitMu.Lock()
|
||||
reqs := make([]edgeservice.SubmitProviderTunnelRequest, len(s.tunnelReqs))
|
||||
copy(reqs, s.tunnelReqs)
|
||||
s.submitMu.Unlock()
|
||||
return reqs
|
||||
}
|
||||
|
||||
func (s *fakeRunService) reqsSnapshot() []edgeservice.SubmitRunRequest {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
return append([]edgeservice.SubmitRunRequest(nil), s.reqs...)
|
||||
}
|
||||
|
||||
func (s *fakeRunService) tunnelBodiesSnapshot() [][]byte {
|
||||
s.submitMu.Lock()
|
||||
defer s.submitMu.Unlock()
|
||||
return append([][]byte(nil), s.tunnelBodies...)
|
||||
}
|
||||
|
||||
// relayProviderFixtureFrames performs the provider HTTP request the way the
|
||||
// Node relay does and converts status/header/body bytes into ordered tunnel
|
||||
// frames, chunking the body at odd boundaries to exercise ordered writes.
|
||||
func relayProviderFixtureFrames(providerURL, method, path string, body []byte) (chan *iop.ProviderTunnelFrame, error) {
|
||||
httpReq, err := http.NewRequest(method, providerURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := make(map[string]string)
|
||||
for k, values := range resp.Header {
|
||||
if len(values) > 0 {
|
||||
headers[k] = values[0]
|
||||
}
|
||||
}
|
||||
|
||||
const chunkSize = 7
|
||||
frames := make(chan *iop.ProviderTunnelFrame, len(respBody)/chunkSize+3)
|
||||
seq := int64(0)
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
RunId: "run-tunnel",
|
||||
TunnelId: "tunnel-1",
|
||||
Sequence: seq,
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
|
||||
StatusCode: int32(resp.StatusCode),
|
||||
Headers: headers,
|
||||
}
|
||||
seq++
|
||||
for off := 0; off < len(respBody); off += chunkSize {
|
||||
end := off + chunkSize
|
||||
if end > len(respBody) {
|
||||
end = len(respBody)
|
||||
}
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
RunId: "run-tunnel",
|
||||
TunnelId: "tunnel-1",
|
||||
Sequence: seq,
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
|
||||
Body: append([]byte(nil), respBody[off:end]...),
|
||||
}
|
||||
seq++
|
||||
}
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
RunId: "run-tunnel",
|
||||
TunnelId: "tunnel-1",
|
||||
Sequence: seq,
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END,
|
||||
End: true,
|
||||
}
|
||||
close(frames)
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
func staticProviderTunnelFrames(body string) chan *iop.ProviderTunnelFrame {
|
||||
frames := make(chan *iop.ProviderTunnelFrame, 3)
|
||||
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START, StatusCode: 200}
|
||||
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY, Body: []byte(body)}
|
||||
frames <- &iop.ProviderTunnelFrame{Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_END, End: true}
|
||||
close(frames)
|
||||
return frames
|
||||
}
|
||||
|
||||
func (s *fakeRunService) CancelRun(_ context.Context, req edgeservice.CancelRunRequest) (edgeservice.CommandResult, error) {
|
||||
s.cancelMu.Lock()
|
||||
s.cancelCalls = append(s.cancelCalls, req)
|
||||
s.cancelMu.Unlock()
|
||||
if s.cancelErr != nil {
|
||||
return edgeservice.CommandResult{}, s.cancelErr
|
||||
}
|
||||
return edgeservice.CommandResult{NodeID: req.NodeRef, SessionID: req.SessionID}, nil
|
||||
}
|
||||
|
||||
func (s *fakeRunService) cancelCallsSnapshot() []edgeservice.CancelRunRequest {
|
||||
s.cancelMu.Lock()
|
||||
defer s.cancelMu.Unlock()
|
||||
return append([]edgeservice.CancelRunRequest(nil), s.cancelCalls...)
|
||||
}
|
||||
|
||||
func (s *fakeRunService) SubmitRun(_ context.Context, req edgeservice.SubmitRunRequest) (edgeservice.RunResult, error) {
|
||||
s.submitMu.Lock()
|
||||
s.req = req
|
||||
s.reqs = append(s.reqs, req)
|
||||
if s.submitErr != nil {
|
||||
err := s.submitErr
|
||||
s.submitMu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
attempt := len(s.reqs)
|
||||
if s.submitErrAfter > 0 && attempt >= s.submitErrAfter {
|
||||
s.submitMu.Unlock()
|
||||
return nil, fmt.Errorf("inject error after %d attempts", attempt-1)
|
||||
}
|
||||
events := s.events
|
||||
if attempt-1 < len(s.eventRuns) {
|
||||
events = s.eventRuns[attempt-1]
|
||||
}
|
||||
runID := "run-test"
|
||||
if attempt-1 < len(s.runIDs) {
|
||||
runID = s.runIDs[attempt-1]
|
||||
} else if attempt > 1 {
|
||||
runID = fmt.Sprintf("run-test-%d", attempt)
|
||||
}
|
||||
s.submitMu.Unlock()
|
||||
return &edgeservice.RunHandle{
|
||||
RunDispatch: edgeservice.RunDispatch{
|
||||
RunID: runID,
|
||||
ModelGroupKey: req.ModelGroupKey,
|
||||
Adapter: req.Adapter,
|
||||
Target: req.Target,
|
||||
SessionID: req.SessionID,
|
||||
TimeoutSec: 5,
|
||||
},
|
||||
RunStream: edgeservice.RunStream{
|
||||
Events: events,
|
||||
NodeEvents: make(chan *iop.EdgeNodeEvent),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *fakeRunService) OllamaAPI(_ context.Context, req edgeservice.OllamaAPIRequest) (edgeservice.OllamaAPIView, error) {
|
||||
s.ollamaReq = req
|
||||
if s.ollamaResp.StatusCode == 0 {
|
||||
s.ollamaResp.StatusCode = http.StatusOK
|
||||
}
|
||||
return s.ollamaResp, nil
|
||||
}
|
||||
|
||||
func bufferedRunEvents(events ...*iop.RunEvent) chan *iop.RunEvent {
|
||||
ch := make(chan *iop.RunEvent, len(events))
|
||||
for _, event := range events {
|
||||
ch <- event
|
||||
}
|
||||
return ch
|
||||
}
|
||||
BIN
edge
BIN
edge
Binary file not shown.
BIN
node
BIN
node
Binary file not shown.
BIN
openai.test
BIN
openai.test
Binary file not shown.
Loading…
Reference in a new issue