Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .issueops/CAUTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ footer; older notes also live in `archive/cautions-incidents.md`.

| Date | Lesson |
|---|---|
| 2026-10-15 | [Linux에서 병렬 테스트가 방금 쓴 스크립트를 exec하면 text file busy(ETXTBSY)로 확률적으로 실패하고 macOS에서는 재현되지 않는다](cautions/2026-10-15-linux-exec-text-file-busy.md) |
| 2026-10-14 | [`execution complete`는 봉인하는 head가 검증받은 변경 집합인지 보지 않으므로, 보고서를 쓰기 전에 `_stale`·PR 뒤 코드 커밋을 확인한다](cautions/2026-10-14-completion-does-not-check-the-head-was-verified.md) |
| 2026-10-13 | [변이 대응 테스트는 호출자 경계와 호출자가 만들 수 있는 입력만 써야 하고, gremlins v0.6.0은 `issueops/` 아래 변이를 NOT COVERED로 잘못 보고한다](cautions/2026-10-13-mutation-tests-must-stay-on-the-consumer-boundary.md) |
| 2026-10-12 | [`bytes.Buffer`를 임베딩한 상한 writer는 `ReadFrom`으로 상한을 우회하고, map 리터럴 순회 검증은 오류 필드가 실행마다 바뀐다](cautions/2026-10-12-bounded-writers-and-map-ordered-messages.md) |
Expand Down
20 changes: 20 additions & 0 deletions .issueops/cautions/2026-10-15-linux-exec-text-file-busy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: 2026-10-15-linux-exec-text-file-busy
description: Caution record for a solved false case or recurring risk.
---

# Linux에서 병렬 테스트가 방금 쓴 스크립트를 exec하면 text file busy로 실패한다

- Date: 2026-10-15
- Kind: `caution`
- Source: issueops-docs io-9e1b5fa96747 (#563)
- Summary: 스크립트를 os.WriteFile로 쓴 직후 exec하는 테스트를 t.Parallel로 돌리면, 다른 병렬 테스트의 fork가 쓰기 fd를 잠깐 복제해 Linux에서 exec가 ETXTBSY(text file busy)로 확률적으로 실패한다. macOS에서는 재현되지 않는다.
- Context: #563 PR #564의 pull_request CI에서 internal/adapter/update TestRuntimeInstall이 'fork/exec .../install-native.sh: text file busy'로 두 번 연속 실패했다. 같은 head의 push run은 통과했다. lint 실패에 가려 Race test 단계가 한동안 돌지 않아 늦게 드러났다. Linux arm64 test binary를 ubuntu:24.04 컨테이너에서 -test.count=300 -test.cpu=4 -test.shuffle=on으로 돌리면 재현됐다(TestProcessTableRead, TestRuntimeInstall, TestRuntimeList 실패).
- Resolution: 스크립트를 쓰고 곧바로 실행하는 테스트(TestProcessTableRead, TestRuntimeInstall, TestRuntimeList와 그 하위 테스트)에서 t.Parallel을 뺐다. 직렬 테스트는 병렬 테스트가 재개되기 전에 끝나고 병렬로 남은 테스트는 fork하지 않으므로 창이 사라진다. 같은 컨테이너 실행(count 1500, cpu 2,8,16, shuffle)이 text file busy 없이 통과했고 #563 게이트 원장 G5가 그 실행을 고정한다. macOS 로컬 실행만으로는 이 부류를 확인할 수 없으므로 exec 테스트를 병렬화할 때는 Linux 컨테이너에서 확인한다.
- Evidence:
- internal/adapter/update/runtime_test.go (writeScript 주석)
- .issueops/issues/563/gates.md (G5)
- go test -c GOOS=linux 바이너리를 ubuntu:24.04 컨테이너에서 실행: 수정 전 text file busy 재현, 수정 후 -test.count=1500 통과
- Alternatives / rejected options:
- writeScript 뒤 exec를 ETXTBSY 재시도로 감싸기: 원인을 두고 증상을 가린다
- 병렬 테스트 전체를 직렬로 바꾸기: fork하지 않는 테스트까지 늦춘다
22 changes: 22 additions & 0 deletions .issueops/issues/563/gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Gates: 563

- [x] G1: CI와 같은 Linux·go.mod toolchain·golangci-lint v2.12.2 lint가 지적 없이 끝난다
CHECK: python3 -c "import os,subprocess,sys,tempfile; d=tempfile.mkdtemp(); v=subprocess.run(['go','list','-m','-f','{{.GoVersion}}'],capture_output=True,text=True,check=True).stdout.strip(); subprocess.run(['go','install','github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2'],env=dict(os.environ,GOBIN=d),check=True); r=subprocess.run([d+'/golangci-lint','run','./...'],env=dict(os.environ,GOOS='linux',GOTOOLCHAIN='go'+v),capture_output=True,text=True); out=r.stdout.strip(); print(out.splitlines()[-1] if out else r.stderr.strip()[-300:]); sys.exit(r.returncode)"
EXPECT: 0 issues.
EVIDENCE: 0 issues.
- [x] G2: gofmt·go vet·go build·race 전체 테스트가 모두 통과한다
CHECK: python3 -c "import subprocess,sys; fmt=subprocess.run(['gofmt','-l','cmd','internal'],capture_output=True,text=True).stdout.strip(); cmds=[['go','vet','./...'],['go','build','-o','/dev/null','./cmd/issueops'],['go','test','-race','./...','-count=1']]; bad=[' '.join(c[:2]) for c in cmds if subprocess.run(c,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode]; bad=(['gofmt'] if fmt else [])+bad; print('ALL_PASS' if not bad else 'FAIL '+','.join(bad)); sys.exit(1 if bad else 0)"
EXPECT: ALL_PASS
EVIDENCE: ALL_PASS
- [x] G3: lint 설정·nolint 억제가 늘지 않고 production Go 파일이 바뀌지 않는다
CHECK: python3 -c "import subprocess,sys; run=lambda a: subprocess.run(a,capture_output=True,text=True,check=True).stdout; base=run(['git','merge-base','HEAD','origin/main']).strip(); assert base; cfg=run(['git','diff',base,'--','.golangci.yml']); diff=run(['git','diff',base,'--unified=0','--','*.go']).splitlines(); sup=[l for l in diff if l.startswith('+') and 'nolint' in l]; prod=[p for p in run(['git','diff','--name-only',base,'--','*.go']).split() if not p.endswith('_test.go')]; bad=bool(cfg or sup or prod); print('SUPPRESSION_OR_PROD_CHANGE' if bad else 'NO_SUPPRESSION'); sys.exit(1 if bad else 0)"
EXPECT: NO_SUPPRESSION
EVIDENCE: NO_SUPPRESSION
- [x] G4: self-verify의 모든 단계(go test ./... -count=1 포함)가 통과한다
CHECK: python3 -c "import os,shutil,subprocess as s,json,tempfile; e=lambda k: s.run(['go','env',k],capture_output=True,text=True).stdout.strip(); t=tempfile.mkdtemp(); env=dict(os.environ,HOME=t,CODEX_HOME=t+'/.codex',GOCACHE=e('GOCACHE'),GOMODCACHE=e('GOMODCACHE'),GOPATH=e('GOPATH')); b=s.run(['go','build','-o','bin/issueops','./cmd/issueops'],capture_output=True,text=True); i=s.run(['./scripts/install-native.sh','--skip-build','--path-mode=skip','--mcp-transport=stdio'],env=env,capture_output=True,text=True); p=s.run(['./bin/issueops','self-verify','--seed=100','--target-score=95','--llm-eval=false','--json'],env=env,capture_output=True,text=True); d=json.loads(p.stdout or '{}'); s.run(['chmod','-R','u+w',t]); shutil.rmtree(t,ignore_errors=True); print('SELF_VERIFY_OK='+str(b.returncode==0 and i.returncode==0 and bool(d.get('ok'))))"
EXPECT: SELF_VERIFY_OK=True
EVIDENCE: SELF_VERIFY_OK=True
- [x] G5: update 패키지 테스트가 Linux 컨테이너에서 병렬 fork 압박을 받아도 text file busy 없이 통과한다
CHECK: python3 -c "import os,platform,subprocess,sys,tempfile; a='arm64' if platform.machine() in ('arm64','aarch64') else 'amd64'; v=subprocess.run(['go','list','-m','-f','{{.GoVersion}}'],capture_output=True,text=True,check=True).stdout.strip(); d=tempfile.mkdtemp(dir='/tmp'); b=d+'/update.test'; c=subprocess.run(['go','test','-c','-o',b,'./internal/adapter/update'],env=dict(os.environ,GOOS='linux',GOARCH=a,GOTOOLCHAIN='go'+v),capture_output=True,text=True); r=subprocess.run(['docker','run','--rm','-v',b+':/t:ro','ubuntu:24.04','/t','-test.count=1500','-test.cpu=2,8,16','-test.shuffle=on'],capture_output=True,text=True) if c.returncode==0 else c; out=r.stdout+r.stderr; ok=c.returncode==0 and r.returncode==0 and 'text file busy' not in out; print('ETXTBSY_FREE' if ok else 'FAIL '+out[-300:]); sys.exit(0 if ok else 1)"
EXPECT: ETXTBSY_FREE
EVIDENCE: ETXTBSY_FREE
28 changes: 28 additions & 0 deletions .issueops/issues/563/intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 요청자 의도 계약

- lifecycle: io-9e1b5fa96747
- issue: https://github.com/m16khb-org/issueops/issues/563
- intent_class: standard

## 원문 요청
1

(직전 선택지 원문) 1. main의 lint 부채를 별도 사이클에서 먼저 고친다 (권장): 고친 뒤 이 사이클은 `sync-base`로 base를 맞추고 reseed해서 새 head로 CI를 다시 받은 다음 머지합니다. 범위가 섞이지 않습니다. 참고로 #560(orca 연동 제거, 현재 plan 단계)이 머지되면 orca 관련 지적이 함께 사라집니다. 그래도 trace, issueopsapp, sqlstore 등에 남은 지적은 따로 고쳐야 합니다.

## 해석
main(bac123aa)의 CI verify job이 2026-10-12 dd271b8b 이후 golangci-lint v2.12.2 단계에서 90건(errcheck 62, govet inline 25, unused 3)으로 실패해 Build·race test·self-verify 단계가 skipped된다. 이 때문에 PR #562(#561)를 포함한 모든 PR이 checks_failing으로 머지되지 못한다. 별도 사이클에서 지적 90건을 코드와 테스트에서 직접 고쳐(설정 제외·nolint 억제 없이) main CI를 다시 green으로 만든다. 이후 #561 사이클은 sync-base와 reseed로 새 head에서 CI를 다시 받아 머지한다(이번 사이클 범위 밖).

## 성공 기준
- CI와 같은 GOOS=linux, go.mod toolchain의 golangci-lint v2.12.2 run ./... 이 0 issues로 끝난다; go vet ./..., go test ./... -count=1, go test -race ./... -count=1, go build ./cmd/issueops 가 통과한다; 이 사이클 PR의 GitHub Actions CI verify job이 lint 이후 단계까지 모두 success다; .golangci.yml의 linter·제외 설정과 nolint 주석은 늘리지 않는다.

## 비목표
- (없음)

## 제약
- (없음)

## 모호함
- (없음)

## 읽는 규칙
이 문서는 요청자 의도 계약이다. 원격 이슈 본문은 구현 계약이다. 두 문서가 충돌하면 구현을 시작하지 말고 충돌한 줄을 인용해 blocker로 보고한다.
13 changes: 13 additions & 0 deletions .issueops/issues/563/plan-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 계획 검토 기록

## 1차: 수정 요청

- plan-review anthropic/claude-opus-5-5 high: 계획의 사실 주장(지적 90건의 파일별 분포, requireOrcaCode 85곳 중 52곳이 반환값을 버린다는 계산, go fix -inline이 테스트 파일만 고친다는 가정)을 lint 출력·CI 로그·go fix -inline -diff로 대조했고 모두 맞았다.
- 반환값을 버리던 단언 helper를 나누는 설계는 기존 판정과 t.Fatalf를 유지해 단언 강도가 약해지지 않는다.
- 게이트가 성공 기준의 go test ./... -count=1과 저장소 최소 완료 기준인 self-verify를 관측하지 않는다. CI의 lint 뒤 단계가 그동안 돌지 않았으므로 self-verify 게이트를 더하도록 수정을 요청했다.

## 2차: 통과

- plan-review anthropic/claude-opus-5-5 high (2차 delta): 수정된 계획은 self-verify 게이트를 race 게이트와 분리해 추가했다. 저장소 문서가 self-verify 통과에 go test ./... -count=1 책임을 맡기므로 직전 라운드의 성공 기준 공백은 해소됐다.
- 바뀐 게이트를 직접 실행해 확인했다. lint toolchain은 go.mod에서 go1.26.6으로 읽히고, 억제 검사 게이트는 git이 실패하면 0이 아닌 종료 코드로 끝나며, 게이트 lint는 네 게이트 모두 problems 0이었다.
- 비차단 메모: 커밋 전 작업 트리에서 self-verify는 전체 테스트 대신 risk QA의 race 전체 실행 결과를 재사용하고, PR 발행 전 CI 확인은 gh pr checks가 아니라 HEAD 커밋의 gh run list로 해야 한다.
Loading
Loading