.mam.env 를 bash 와 Python 이 서로 다른 규칙으로 읽어서, 한쪽에서 정상인 줄이 다른 쪽에서 치명적이던 문제를 고친다. 같은 PR 에 두 가지를 함께 담았다. 정수 환경변수를 10진수로 정규화하는 것과, 공개 MQTT 브로커에 기대던 테스트를 없애는 것이다.
A. .mam.env 를 셸 source 대신 Python 파서로 읽는다
파서는 하나다. 새 lib_py/delegation/envfile.py(표준 라이브러리만 사용)가 규칙을 한 곳에 담는다. 주석과 빈 줄은 건너뛰고, 키와 값의 앞뒤 공백을 떼고, 감싸는 따옴표 한 쌍을 벗기고, 셸 해석은 하지 않는다. Python 쪽 mqtt_common._parse_env_file 도 이 모듈에 위임한다.
형식은 파서가 강제한다. 키는 ^[A-Za-z_][A-Za-z0-9_]*$ 여야 하고, 어긋나거나 = 가 없는 줄은 줄 번호와 함께 경고하고 건너뛴다. 경고에 값은 찍지 않는다.
bash 자체 변수는 받지 않는다. 따옴표로 감싸 내보내도 RANDOM=a[$(cmd)] 같은 bash 특수 변수 대입은 산술 평가되면서 명령을 실행한다(1회차 codex 리뷰 지적). bash 가 정의하는 변수와 BASH_·COMP_·READLINE_ 접두어는 두 로더 모두 경고하고 건너뛰며, 깨끗한 bash 가 정의하는 모든 변수를 덮는지 테스트로 고정했다. 래퍼가 이미 설정한 SCRIPT_DIR·REPO_ROOT·TARGET_ENV 도 파일이 바꿀 수 없다.
래퍼는 파서를 호출한다. 파서가 shlex 로 감싼 export 문을 만들고 래퍼가 그것을 평가한다. 값이 명령줄 인자에 오르지 않는다.
명령줄 환경변수가 파일 값보다 우선한다. 전에는 Python 로더는 이미 있는 값을 지켰는데 래퍼의 set -a; source 는 덮어써서, 같은 실행에서 두 쪽이 다른 값을 쓸 수 있었다. 이슈에 없던 차이로, 이번에 함께 맞췄다.
이슈 작업 지시에는 "세 곳의 source"로 적혀 있었으나, 저장소 전체를 조사한 결과 셸이 env 파일을 해석하는 곳은 delegate-job 래퍼 한 곳(:38)뿐이었다.
B. 정수 환경변수를 10진수로 읽는다
bash 는 0 으로 시작하는 수를 8진수로 읽는다. MAM_JOB_TIMEOUT_S=0700 은 래퍼에서 조용히 448초가 됐고 0900 은 산술 오류로 죽었다. 루프 러너는 같은 값을 700 으로 읽어 두 쪽이 어긋났다.
규칙을 하나로 맞췄다. bash 쪽 lib.sh 의 mam_int·mam_int_knob 와 러너의 _positive_int 가 같은 규칙으로 읽는다. 앞뒤 공백 허용, 숫자만, 10진수, 유효 자릿수 9 자리 이하, 값마다 정한 최솟값 이상.
최솟값 1: MAM_JOB_TIMEOUT_S, MAM_HEARTBEAT_SEC, MAM_HEARTBEAT_MISSES, MAM_ACK_TIMEOUT_S, 그리고 래퍼의 해당 플래그들. 잘못된 환경변수는 경고 후 기본값, 잘못된 플래그는 종료 코드 1.
tests/test_issue26_ints.py 의 표가 각 값의 최솟값을 정의하고, 정적 검사가 셸 코드에서 정규화되지 않은 정수 환경변수가 산술에 들어가는 것을 막는다.
C. 공개 브로커에 의존하는 테스트를 없앤다
작업 지시는 "5종"이었으나 조사해 보니 공개 브로커(broker.hivemq.com) 왕복에 기대는 테스트는 22개였다.
단순히 로그 단언으로 바꾸는 대신, 테스트 안에서 도는 로컬 MQTT 3.1.1 브로커(tests/_local_mqtt.py, 127.0.0.1)를 만들어 22개 모두가 그것을 쓰게 했다. 실제 MQTT 왕복은 그대로 검증하면서 외부 의존만 없앤다. "브로커에 닿지 않음"으로 건너뛰던 경로는 사라졌다. 로컬 브로커가 배달하지 못하면 실패다.
그중 일부(#18 acceptance1·5, #19)는 실제 생산자인 구독자·러너·감사 로그에 남은 기록으로도 단언한다.
tests/test_local_mqtt.py 가 실제 클라이언트와 실제 publish_event → job_subscriber 경로로 브로커의 적합성을 확인하고, 테스트 코드에 공개 브로커 주소가 다시 들어오지 않도록 정적 검사를 둔다.
검증
항목
결과
전체 스위트 (34241d7)
1273 통과 / 13 실패 / 19 건너뜀
리뷰
아래 표
.mam.env 명령 실행 재현
수정 전 명령 2개 실행, 수정 후 실행 없음
실패 13건 중 12건은 이 호스트의 기준 실패(tests/test_issue10_13_purge.py)다. 나머지 1건(test_bug4_send_keys_safe_gating_order)은 함수 앞 2500자만 잘라 검사하던 테스트가 정규화 줄 추가로 대상을 놓친 것이며, 검사 순서 자체는 그대로였다. fb00637 이 함수 본문 전체를 보도록 고쳤다. 이후 커밋은 테스트 파일만 바꿨고, 해당 테스트를 각각 따로 확인했다. 전체 로그에 브로커 때문에 건너뛴 테스트는 없다.
설정 파일로 명령이 실행되는지 실제 래퍼로 재현했다.
설정 파일
수정 전 e1eb22b
수정 후 34241d7
첫 줄이 KEY= value
그 줄에서 즉시 종료 (rc 127)
정상 (rc 0)
$(touch …), RANDOM=a[$(touch …)] 등
명령 2개 실행
실행 없음, 줄 번호 경고 후 건너뜀
리뷰.
리뷰어
브랜치 전체 fb00637
델타 b58edac (테스트만)
claude
NOT PASS (아래)
PASS
agy
PASS
PASS
grok
PASS
PASS
opencode
PASS
사용량 한도로 판정 불가
codex
판정 불가
제외
codex 는 1회차에 NOT PASS 로 세 가지를 지적했다. bash 특수 변수 대입을 통한 명령 실행, SKS_QUIESCENT_TRIES 의 산술 노출, 매우 긴 10진수 문자열이다. 34241d7 이 모두 반영했다. 그런데 이후 codex 는 OpenAI 의 보안 관련 콘텐츠 제한("Some cybersecurity requests may still be limited")에 막혀 판정을 내지 못했다. 이 PR 이 설정 파일을 통한 명령 실행을 막는 작업이라 그렇게 분류된 것으로 보인다. 가장 심각한 첫 지적은 위 표처럼 실제 래퍼로 수정 전후를 재현해 확인했다.
claude 는 브랜치 전체 리뷰에서 이 PR 이 새로 넣은 test_c0_make_client_speaks_3_1_1_wildcards_qos1_and_retained 의 경쟁 조건을 찾았다. paho 의 connect() 는 CONNACK 을 기다리지 않는데, 테스트가 연결 직후 브로커 상태를 단언했다. 브로커의 연결 처리를 늦춰 결정적으로 재현했다. b58edac 가 테스트 클라이언트가 CONNACK·UNSUBACK 을 기다리고, 클라이언트별로 단언하고, 로컬 브로커가 PUBLISH 를 배달한 뒤 PUBACK 을 보내도록 고쳤다. 브로커를 늦춘 상태의 회귀 테스트도 넣었다.
opencode 는 브랜치 전체에 PASS 를 냈고, 테스트만 바뀐 마지막 델타는 Ollama 세션 사용량 한도로 판정하지 못했다.
Closes #26
`.mam.env` 를 bash 와 Python 이 서로 다른 규칙으로 읽어서, 한쪽에서 정상인 줄이 다른 쪽에서 치명적이던 문제를 고친다. 같은 PR 에 두 가지를 함께 담았다. 정수 환경변수를 10진수로 정규화하는 것과, 공개 MQTT 브로커에 기대던 테스트를 없애는 것이다.
## A. `.mam.env` 를 셸 `source` 대신 Python 파서로 읽는다
- **파서는 하나다.** 새 `lib_py/delegation/envfile.py`(표준 라이브러리만 사용)가 규칙을 한 곳에 담는다. 주석과 빈 줄은 건너뛰고, 키와 값의 앞뒤 공백을 떼고, 감싸는 따옴표 한 쌍을 벗기고, 셸 해석은 하지 않는다. Python 쪽 `mqtt_common._parse_env_file` 도 이 모듈에 위임한다.
- **형식은 파서가 강제한다.** 키는 `^[A-Za-z_][A-Za-z0-9_]*$` 여야 하고, 어긋나거나 `=` 가 없는 줄은 줄 번호와 함께 경고하고 건너뛴다. 경고에 값은 찍지 않는다.
- **bash 자체 변수는 받지 않는다.** 따옴표로 감싸 내보내도 `RANDOM=a[$(cmd)]` 같은 bash 특수 변수 대입은 산술 평가되면서 명령을 실행한다(1회차 codex 리뷰 지적). bash 가 정의하는 변수와 `BASH_`·`COMP_`·`READLINE_` 접두어는 두 로더 모두 경고하고 건너뛰며, 깨끗한 bash 가 정의하는 모든 변수를 덮는지 테스트로 고정했다. 래퍼가 이미 설정한 `SCRIPT_DIR`·`REPO_ROOT`·`TARGET_ENV` 도 파일이 바꿀 수 없다.
- **래퍼는 파서를 호출한다.** 파서가 `shlex` 로 감싼 export 문을 만들고 래퍼가 그것을 평가한다. 값이 명령줄 인자에 오르지 않는다.
- **명령줄 환경변수가 파일 값보다 우선한다.** 전에는 Python 로더는 이미 있는 값을 지켰는데 래퍼의 `set -a; source` 는 덮어써서, 같은 실행에서 두 쪽이 다른 값을 쓸 수 있었다. 이슈에 없던 차이로, 이번에 함께 맞췄다.
- 이슈 작업 지시에는 "세 곳의 `source`"로 적혀 있었으나, 저장소 전체를 조사한 결과 셸이 env 파일을 해석하는 곳은 delegate-job 래퍼 한 곳(`:38`)뿐이었다.
## B. 정수 환경변수를 10진수로 읽는다
bash 는 0 으로 시작하는 수를 8진수로 읽는다. `MAM_JOB_TIMEOUT_S=0700` 은 래퍼에서 **조용히 448초**가 됐고 `0900` 은 산술 오류로 죽었다. 루프 러너는 같은 값을 700 으로 읽어 두 쪽이 어긋났다.
- 규칙을 하나로 맞췄다. bash 쪽 `lib.sh` 의 `mam_int`·`mam_int_knob` 와 러너의 `_positive_int` 가 같은 규칙으로 읽는다. 앞뒤 공백 허용, 숫자만, 10진수, 유효 자릿수 9 자리 이하, 값마다 정한 최솟값 이상.
- 최솟값 1: `MAM_JOB_TIMEOUT_S`, `MAM_HEARTBEAT_SEC`, `MAM_HEARTBEAT_MISSES`, `MAM_ACK_TIMEOUT_S`, 그리고 래퍼의 해당 플래그들. 잘못된 환경변수는 경고 후 기본값, 잘못된 플래그는 종료 코드 1.
- 최솟값 0(0 은 "한 번만 확인"): `MAM_INJECT_ACK_GRACE_S`, `SKS_DIALOG_TIMEOUT`, `SKS_BUSY_TIMEOUT`, `SKS_DELIVERY_GRACE_S`, `SKS_QUIESCENT_TRIES` 등.
- `tests/test_issue26_ints.py` 의 표가 각 값의 최솟값을 정의하고, 정적 검사가 셸 코드에서 정규화되지 않은 정수 환경변수가 산술에 들어가는 것을 막는다.
## C. 공개 브로커에 의존하는 테스트를 없앤다
- 작업 지시는 "5종"이었으나 조사해 보니 공개 브로커(`broker.hivemq.com`) 왕복에 기대는 테스트는 **22개**였다.
- 단순히 로그 단언으로 바꾸는 대신, 테스트 안에서 도는 **로컬 MQTT 3.1.1 브로커**(`tests/_local_mqtt.py`, 127.0.0.1)를 만들어 22개 모두가 그것을 쓰게 했다. 실제 MQTT 왕복은 그대로 검증하면서 외부 의존만 없앤다. "브로커에 닿지 않음"으로 건너뛰던 경로는 사라졌다. 로컬 브로커가 배달하지 못하면 실패다.
- 그중 일부(#18 acceptance1·5, #19)는 실제 생산자인 구독자·러너·감사 로그에 남은 기록으로도 단언한다.
- `tests/test_local_mqtt.py` 가 실제 클라이언트와 실제 `publish_event` → `job_subscriber` 경로로 브로커의 적합성을 확인하고, 테스트 코드에 공개 브로커 주소가 다시 들어오지 않도록 정적 검사를 둔다.
## 검증
| 항목 | 결과 |
|---|---|
| 전체 스위트 (`34241d7`) | 1273 통과 / 13 실패 / 19 건너뜀 |
| 리뷰 | 아래 표 |
| `.mam.env` 명령 실행 재현 | 수정 전 명령 2개 실행, 수정 후 실행 없음 |
실패 13건 중 12건은 이 호스트의 기준 실패(`tests/test_issue10_13_purge.py`)다. 나머지 1건(`test_bug4_send_keys_safe_gating_order`)은 함수 앞 2500자만 잘라 검사하던 테스트가 정규화 줄 추가로 대상을 놓친 것이며, 검사 순서 자체는 그대로였다. `fb00637` 이 함수 본문 전체를 보도록 고쳤다. 이후 커밋은 테스트 파일만 바꿨고, 해당 테스트를 각각 따로 확인했다. 전체 로그에 브로커 때문에 건너뛴 테스트는 없다.
**설정 파일로 명령이 실행되는지 실제 래퍼로 재현했다.**
| 설정 파일 | 수정 전 `e1eb22b` | 수정 후 `34241d7` |
|---|---|---|
| 첫 줄이 `KEY= value` | 그 줄에서 즉시 종료 (rc 127) | 정상 (rc 0) |
| `$(touch …)`, `RANDOM=a[$(touch …)]` 등 | **명령 2개 실행** | 실행 없음, 줄 번호 경고 후 건너뜀 |
**리뷰.**
| 리뷰어 | 브랜치 전체 `fb00637` | 델타 `b58edac` (테스트만) |
|---|---|---|
| claude | NOT PASS (아래) | PASS |
| agy | PASS | PASS |
| grok | PASS | PASS |
| opencode | PASS | 사용량 한도로 판정 불가 |
| codex | 판정 불가 | 제외 |
- **codex** 는 1회차에 NOT PASS 로 세 가지를 지적했다. bash 특수 변수 대입을 통한 명령 실행, `SKS_QUIESCENT_TRIES` 의 산술 노출, 매우 긴 10진수 문자열이다. `34241d7` 이 모두 반영했다. 그런데 이후 codex 는 OpenAI 의 보안 관련 콘텐츠 제한("Some cybersecurity requests may still be limited")에 막혀 판정을 내지 못했다. 이 PR 이 설정 파일을 통한 명령 실행을 막는 작업이라 그렇게 분류된 것으로 보인다. 가장 심각한 첫 지적은 위 표처럼 실제 래퍼로 수정 전후를 재현해 확인했다.
- **claude** 는 브랜치 전체 리뷰에서 이 PR 이 새로 넣은 `test_c0_make_client_speaks_3_1_1_wildcards_qos1_and_retained` 의 경쟁 조건을 찾았다. paho 의 `connect()` 는 CONNACK 을 기다리지 않는데, 테스트가 연결 직후 브로커 상태를 단언했다. 브로커의 연결 처리를 늦춰 결정적으로 재현했다. `b58edac` 가 테스트 클라이언트가 CONNACK·UNSUBACK 을 기다리고, 클라이언트별로 단언하고, 로컬 브로커가 PUBLISH 를 배달한 뒤 PUBACK 을 보내도록 고쳤다. 브로커를 늦춘 상태의 회귀 테스트도 넣었다.
- **opencode** 는 브랜치 전체에 PASS 를 냈고, 테스트만 바뀐 마지막 델타는 Ollama 세션 사용량 한도로 판정하지 못했다.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- lib_py/delegation/envfile.py (stdlib only) holds the one set of rules:
comments and blank lines skipped, key and value stripped, one matching
quote pair removed, no shell interpretation, keys must match
^[A-Za-z_][A-Za-z0-9_]*$ (bad or `=`-less lines warn with the line
number, never the value).
- mqtt_common._parse_env_file delegates to envfile.apply.
- The delegate-job wrapper runs envfile.py --shell by path and evals the
shlex-quoted exports; values never reach a command line. The environment
now wins over the file in both loaders.
- .mam.env.example: commented assignments no longer use $HOME, which the
parser keeps literally.
The shell parsed the env file in one place only (wrapper :38), not three.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bash read MAM_JOB_TIMEOUT_S=0700 as octal 448 (and failed on 0900) while
the loop runner read 700. One rule now, with a per-knob minimum:
lib.sh mam_int / mam_int_knob and runner._positive_int (blanks allowed,
digits only, decimal, min <= n <= 999999999).
- minimum 1: MAM_JOB_TIMEOUT_S, MAM_HEARTBEAT_SEC, MAM_HEARTBEAT_MISSES,
MAM_ACK_TIMEOUT_S (invalid env: warning + default) and the wrapper's
--timeout/--idle-timeout/--heartbeat/--heartbeat-misses/--ack-timeout
(invalid: rc 1); the runner's job_budget, --job-timeout, contract_args.
- minimum 0 (0 = check once): MAM_INJECT_ACK_GRACE_S,
delegate_wait_started's ack_s, SKS_DIALOG_TIMEOUT, SKS_BUSY_TIMEOUT,
SKS_DELIVERY_GRACE_S.
- tests/test_issue26_ints.py: INT_KNOBS is the one table of minimums; a
static guard keeps env defaults out of arithmetic and checks each
normalised knob against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/_local_mqtt.py: in-process MQTT 3.1.1 broker on 127.0.0.1 (what
the repo uses: protocol level 4, clean session, QoS 0/1, retained).
- conftest: one session broker named by MQTT_BROKER/MQTT_PORT/MQTT_TLS;
other redirecting/auth MQTT_* dropped and restored afterwards.
- Harnesses of the 22 broker-dependent tests (#18 acceptance1/2/3/5,
#6 t1_1/2/7/8/14/15/16/20, #19 MQTT loop x2, #33 b3/r3/b7, #30
s9/s10/r2) use it; the "broker unreachable" skips are gone (a local
broker that does not deliver is a failure). acceptance1/5 and #19 also
assert on the subscriber, runner and audit logs the real producers write.
- tests/test_local_mqtt.py: conformance with the real clients and the real
publish_event -> job_subscriber path (C0), an AST guard against public
broker literals and MQTT_* deleted without a replacement (C2), and the
session's broker config (C3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R1 (P1): a quoted export still ran code when the key was a bash special
variable (RANDOM=a[$(cmd)] is evaluated arithmetically on assignment).
envfile.RESERVED (bash's own variables, plus BASH_/COMP_/READLINE_
prefixes) is skipped with a line-numbered warning by both loaders; a test
keeps it covering every variable a clean bash defines. The wrapper also
hands SCRIPT_DIR/REPO_ROOT/TARGET_ENV to the parser as environment, so the
file cannot replace the variables it has already set.
R2 (P2): SKS_QUIESCENT_TRIES reached `for ((i < tries))` through a function
argument; it is normalised at the call site (minimum 0). B5 now requires
every integer-looking env default in the shell code to be normalised or
listed in NOT_ARITHMETIC with where it goes, and exercises _pane_quiescent.
R3 (P2): _positive_int strips leading zeros and rejects more than nine
significant digits before int(), like mam_int; 5000-digit inputs no longer
raise in the runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34241d7 added a line near the top of send_keys_safe (SKS_QUIESCENT_TRIES
normalisation), which pushed `agent prompt` from inside the 2500-character
window to offset 2519. The order the test guards is unchanged
(_pane_quiescent < _pane_dialog_open < agent prompt); it now slices up to
the function's closing brace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (fa45b0e6): paho's connect() does not wait for the CONNACK, so C0
could read protocol_levels before the broker had handled the second
CONNECT, and the `before` slice could take in a previous test's connection.
- _Client waits for CONNACK (and UNSUBACK, instead of sleeping 0.3 s);
C0 checks the levels of its own two client ids (stats() levels_by_client).
- The local broker now routes and stores a PUBLISH before its PUBACK, so a
publisher holding the ack may rely on the retained state it asked for
(late subscriber replay, retained delete).
- test_c0_holds_when_the_broker_is_slow runs C0 with CONNECT/UNSUBSCRIBE
delayed and PUBLISH routed late; the old C0 fails it ([] == [4, 4]), as do
an ack-before-route broker and a fixed-sleep unsubscribe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge-workspace review of PR #43 (codex R1-R4, claude R1/R2).
- R1: envfile.parse raises EnvFileError when the file exists but cannot be
read or decoded as a whole; `--shell` prints the cause and exits 1, so the
wrapper stops (rc 1, as before #26) instead of running on defaults, and
mqtt_common's entry points stop on the same error. A bad single line is
still a warning. The wrapper's message no longer claims a read failure
when the cause is elsewhere ("cannot load ... see the line above").
- R2: lib.sh mam_int spells out the ASCII blanks and digits instead of
[[:space:]]/[0-9], which follow the locale: U+2003 30 U+2003 was 30 in the
wrapper and invalid in the runner under en_US.UTF-8. The test oracle
_expected uses the same ASCII rule.
- R3: an integer flag given explicitly with an empty value is refused like
any other invalid value; only an absent flag keeps its default.
- R4: a line holding a NUL byte is skipped with a warning by both loaders;
before, the wrapper read "leftright" and the Python loader died.
- .mam.env.example states the format: no expansion, `export` and bash's own
variables skipped, a trailing `# ...` is part of the value, the environment
wins, an unreadable file is an error.
Tests: unreadable (0000) and invalid-UTF-8 files stop the wrapper and the
Python loader; a NUL line is skipped by both; Unicode blanks/digits are
invalid for mam_int, _positive_int, the wrapper and the runner under C and
en_US.UTF-8; explicit empty values for the five integer flags are refused.
All of them fail on b58edac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu
merged commit 0051fd6c3b into main2026-10-01 15:23:26 +00:00
R1 존재하지만 읽을 수 없는 .mam.env 에서 래퍼가 rc 0 으로 기본값 진행 → EnvFileError. 래퍼 rc 1(#26 이전과 같음), Python 진입점도 멈춤. 개별 잘못된 줄은 여전히 경고 후 건너뜀. 네 리뷰어 모두 "Python 진입점도 멈춘다"에 동의(경고 후 계속하면 계정·서명 없이 공개 기본 브로커로 발행).
R2 UTF-8 로케일에서 U+2003 30 U+2003 을 bash 는 30, 러너는 무효 → mam_int 가 ASCII 공백·숫자를 명시.
R3 빈 값으로 명시한 정수 플래그가 검증 우회 → 플래그 존재를 기록해 거부.
R4 NUL 이 든 줄을 래퍼는 leftright, Python 은 종료 → 두 로더 모두 줄 번호 경고 후 건너뜀.
실패 1건 test_issue21_acceptance.py::test_a8_second_loop_is_refused_before_a_scheduler_exists 는 이 PR 과 무관하다(main 에서도 실패). 테스트가 마커에 쓰는 ps -o lstart 값이 19 일에는 Oct 1 처럼 공백 두 칸인데, 제품 코드(loop_lock.sh)는 공백을 한 칸으로 줄여 비교한다. 매달 19 일에만 실패하고 제품 동작은 정상.
공개 브로커 때문에 실패하거나 건너뛴 테스트 없음. PR 본문의 "purge 12건 기준 실패"는 이 호스트에서 재현되지 않음(claude·grok 도 단독 실행 23 passed).
후속
test_a8 마커의 공백 정리(테스트 한 줄).
Python 진입점의 EnvFileError 를 트레이스백 대신 ERROR: 한 줄로(claude S1).
파일 첫 줄 BOM 이 "잘못된 키"로 건너뛰어짐; 한 파일 안 중복 키는 첫 값이 이김 — 문서화; MESSAGING.md 의 "없는 MAM_ENV_FILE 은 오류" 설명이 실제(건너뜀)와 다름(grok).
MAM_WAIT_GRACE_S·MAM_CHILD_WAIT_S·MAM_JOB_TREE_MAX 를 _positive_int 로(grok); [ -ge ] 전용 노브의 비숫자 값(claude R3); mam_int·플래그 검증을 #17 항목 4 이전 목록에(claude R5).
## 병합 요약 (v4.8.1)
병합 커밋 0051fd6, 릴리스 2e90c80. Closes #26.
### 리뷰 (병합 워크스페이스, 네 리뷰어)
| 회차 | head | claude | agy | grok | codex |
|---|---|---|---|---|---|
| 1 | b58edac | PASS | PASS | 판정 없음 | NOT PASS (R1–R4) |
| 2 | 0eec795 | PASS | PASS | PASS (PR 전체) | PASS |
- **grok 1회차**: 컨텍스트 압축 중 180 초 안에 `started` 를 보내지 못해 잡이 `agent_blocked` 로 닫혔다. 압축 뒤 늦게 낸 리포트(`report-c9ed5b14.md`, 판정에는 미포함)도 codex R1 과 같은 결함을 독립적으로 찾았다.
- **codex**: 작성 워크스페이스에서는 보안 콘텐츠 제한으로 판정을 못 냈지만, 이번에는 "설정 파일 파싱 규칙의 정확성" 브리프로 정상 리뷰했다.
### 1회차 지적과 수정 (0eec795)
- **R1** 존재하지만 읽을 수 없는 `.mam.env` 에서 래퍼가 rc 0 으로 기본값 진행 → `EnvFileError`. 래퍼 rc 1(#26 이전과 같음), Python 진입점도 멈춤. 개별 잘못된 줄은 여전히 경고 후 건너뜀. 네 리뷰어 모두 "Python 진입점도 멈춘다"에 동의(경고 후 계속하면 계정·서명 없이 공개 기본 브로커로 발행).
- **R2** UTF-8 로케일에서 `U+2003 30 U+2003` 을 bash 는 30, 러너는 무효 → `mam_int` 가 ASCII 공백·숫자를 명시.
- **R3** 빈 값으로 명시한 정수 플래그가 검증 우회 → 플래그 존재를 기록해 거부.
- **R4** NUL 이 든 줄을 래퍼는 `leftright`, Python 은 종료 → 두 로더 모두 줄 번호 경고 후 건너뜀.
- claude 비차단 R1·R2: `.mam.env.example` 형식 규칙, 래퍼 메시지.
- 회귀 테스트 8 건, 모두 b58edac 에서 실패.
### 전체 스위트 (MQTT 환경변수 없이)
- 0eec795: 1315 passed / 1 failed / 3 skipped (60 분). b58edac: 1302 passed / 같은 1 failed.
- 실패 1건 `test_issue21_acceptance.py::test_a8_second_loop_is_refused_before_a_scheduler_exists` 는 이 PR 과 무관하다(main 에서도 실패). 테스트가 마커에 쓰는 `ps -o lstart` 값이 1~9 일에는 `Oct 1` 처럼 공백 두 칸인데, 제품 코드(`loop_lock.sh`)는 공백을 한 칸으로 줄여 비교한다. 매달 1~9 일에만 실패하고 제품 동작은 정상.
- 공개 브로커 때문에 실패하거나 건너뛴 테스트 없음. PR 본문의 "purge 12건 기준 실패"는 이 호스트에서 재현되지 않음(claude·grok 도 단독 실행 23 passed).
### 후속
- `test_a8` 마커의 공백 정리(테스트 한 줄).
- Python 진입점의 `EnvFileError` 를 트레이스백 대신 `ERROR:` 한 줄로(claude S1).
- 파일 첫 줄 BOM 이 "잘못된 키"로 건너뛰어짐; 한 파일 안 중복 키는 첫 값이 이김 — 문서화; `MESSAGING.md` 의 "없는 `MAM_ENV_FILE` 은 오류" 설명이 실제(건너뜀)와 다름(grok).
- `MAM_WAIT_GRACE_S`·`MAM_CHILD_WAIT_S`·`MAM_JOB_TREE_MAX` 를 `_positive_int` 로(grok); `[ -ge ]` 전용 노브의 비숫자 값(claude R3); `mam_int`·플래그 검증을 #17 항목 4 이전 목록에(claude R5).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #26
.mam.env를 bash 와 Python 이 서로 다른 규칙으로 읽어서, 한쪽에서 정상인 줄이 다른 쪽에서 치명적이던 문제를 고친다. 같은 PR 에 두 가지를 함께 담았다. 정수 환경변수를 10진수로 정규화하는 것과, 공개 MQTT 브로커에 기대던 테스트를 없애는 것이다.A.
.mam.env를 셸source대신 Python 파서로 읽는다lib_py/delegation/envfile.py(표준 라이브러리만 사용)가 규칙을 한 곳에 담는다. 주석과 빈 줄은 건너뛰고, 키와 값의 앞뒤 공백을 떼고, 감싸는 따옴표 한 쌍을 벗기고, 셸 해석은 하지 않는다. Python 쪽mqtt_common._parse_env_file도 이 모듈에 위임한다.^[A-Za-z_][A-Za-z0-9_]*$여야 하고, 어긋나거나=가 없는 줄은 줄 번호와 함께 경고하고 건너뛴다. 경고에 값은 찍지 않는다.RANDOM=a[$(cmd)]같은 bash 특수 변수 대입은 산술 평가되면서 명령을 실행한다(1회차 codex 리뷰 지적). bash 가 정의하는 변수와BASH_·COMP_·READLINE_접두어는 두 로더 모두 경고하고 건너뛰며, 깨끗한 bash 가 정의하는 모든 변수를 덮는지 테스트로 고정했다. 래퍼가 이미 설정한SCRIPT_DIR·REPO_ROOT·TARGET_ENV도 파일이 바꿀 수 없다.shlex로 감싼 export 문을 만들고 래퍼가 그것을 평가한다. 값이 명령줄 인자에 오르지 않는다.set -a; source는 덮어써서, 같은 실행에서 두 쪽이 다른 값을 쓸 수 있었다. 이슈에 없던 차이로, 이번에 함께 맞췄다.source"로 적혀 있었으나, 저장소 전체를 조사한 결과 셸이 env 파일을 해석하는 곳은 delegate-job 래퍼 한 곳(:38)뿐이었다.B. 정수 환경변수를 10진수로 읽는다
bash 는 0 으로 시작하는 수를 8진수로 읽는다.
MAM_JOB_TIMEOUT_S=0700은 래퍼에서 조용히 448초가 됐고0900은 산술 오류로 죽었다. 루프 러너는 같은 값을 700 으로 읽어 두 쪽이 어긋났다.lib.sh의mam_int·mam_int_knob와 러너의_positive_int가 같은 규칙으로 읽는다. 앞뒤 공백 허용, 숫자만, 10진수, 유효 자릿수 9 자리 이하, 값마다 정한 최솟값 이상.MAM_JOB_TIMEOUT_S,MAM_HEARTBEAT_SEC,MAM_HEARTBEAT_MISSES,MAM_ACK_TIMEOUT_S, 그리고 래퍼의 해당 플래그들. 잘못된 환경변수는 경고 후 기본값, 잘못된 플래그는 종료 코드 1.MAM_INJECT_ACK_GRACE_S,SKS_DIALOG_TIMEOUT,SKS_BUSY_TIMEOUT,SKS_DELIVERY_GRACE_S,SKS_QUIESCENT_TRIES등.tests/test_issue26_ints.py의 표가 각 값의 최솟값을 정의하고, 정적 검사가 셸 코드에서 정규화되지 않은 정수 환경변수가 산술에 들어가는 것을 막는다.C. 공개 브로커에 의존하는 테스트를 없앤다
broker.hivemq.com) 왕복에 기대는 테스트는 22개였다.tests/_local_mqtt.py, 127.0.0.1)를 만들어 22개 모두가 그것을 쓰게 했다. 실제 MQTT 왕복은 그대로 검증하면서 외부 의존만 없앤다. "브로커에 닿지 않음"으로 건너뛰던 경로는 사라졌다. 로컬 브로커가 배달하지 못하면 실패다.tests/test_local_mqtt.py가 실제 클라이언트와 실제publish_event→job_subscriber경로로 브로커의 적합성을 확인하고, 테스트 코드에 공개 브로커 주소가 다시 들어오지 않도록 정적 검사를 둔다.검증
34241d7).mam.env명령 실행 재현실패 13건 중 12건은 이 호스트의 기준 실패(
tests/test_issue10_13_purge.py)다. 나머지 1건(test_bug4_send_keys_safe_gating_order)은 함수 앞 2500자만 잘라 검사하던 테스트가 정규화 줄 추가로 대상을 놓친 것이며, 검사 순서 자체는 그대로였다.fb00637이 함수 본문 전체를 보도록 고쳤다. 이후 커밋은 테스트 파일만 바꿨고, 해당 테스트를 각각 따로 확인했다. 전체 로그에 브로커 때문에 건너뛴 테스트는 없다.설정 파일로 명령이 실행되는지 실제 래퍼로 재현했다.
e1eb22b34241d7KEY= value$(touch …),RANDOM=a[$(touch …)]등리뷰.
fb00637b58edac(테스트만)SKS_QUIESCENT_TRIES의 산술 노출, 매우 긴 10진수 문자열이다.34241d7이 모두 반영했다. 그런데 이후 codex 는 OpenAI 의 보안 관련 콘텐츠 제한("Some cybersecurity requests may still be limited")에 막혀 판정을 내지 못했다. 이 PR 이 설정 파일을 통한 명령 실행을 막는 작업이라 그렇게 분류된 것으로 보인다. 가장 심각한 첫 지적은 위 표처럼 실제 래퍼로 수정 전후를 재현해 확인했다.test_c0_make_client_speaks_3_1_1_wildcards_qos1_and_retained의 경쟁 조건을 찾았다. paho 의connect()는 CONNACK 을 기다리지 않는데, 테스트가 연결 직후 브로커 상태를 단언했다. 브로커의 연결 처리를 늦춰 결정적으로 재현했다.b58edac가 테스트 클라이언트가 CONNACK·UNSUBACK 을 기다리고, 클라이언트별로 단언하고, 로컬 브로커가 PUBLISH 를 배달한 뒤 PUBACK 을 보내도록 고쳤다. 브로커를 늦춘 상태의 회귀 테스트도 넣었다.🤖 Generated with Claude Code
sourcef1331d36ce병합 요약 (v4.8.1)
병합 커밋
0051fd6, 릴리스2e90c80. Closes #26.리뷰 (병합 워크스페이스, 네 리뷰어)
b58edac0eec795started를 보내지 못해 잡이agent_blocked로 닫혔다. 압축 뒤 늦게 낸 리포트(report-c9ed5b14.md, 판정에는 미포함)도 codex R1 과 같은 결함을 독립적으로 찾았다.1회차 지적과 수정 (
0eec795).mam.env에서 래퍼가 rc 0 으로 기본값 진행 →EnvFileError. 래퍼 rc 1(#26 이전과 같음), Python 진입점도 멈춤. 개별 잘못된 줄은 여전히 경고 후 건너뜀. 네 리뷰어 모두 "Python 진입점도 멈춘다"에 동의(경고 후 계속하면 계정·서명 없이 공개 기본 브로커로 발행).U+2003 30 U+2003을 bash 는 30, 러너는 무효 →mam_int가 ASCII 공백·숫자를 명시.leftright, Python 은 종료 → 두 로더 모두 줄 번호 경고 후 건너뜀..mam.env.example형식 규칙, 래퍼 메시지.b58edac에서 실패.전체 스위트 (MQTT 환경변수 없이)
0eec795: 1315 passed / 1 failed / 3 skipped (60 분).b58edac: 1302 passed / 같은 1 failed.test_issue21_acceptance.py::test_a8_second_loop_is_refused_before_a_scheduler_exists는 이 PR 과 무관하다(main 에서도 실패). 테스트가 마커에 쓰는ps -o lstart값이 19 일에는9 일에만 실패하고 제품 동작은 정상.Oct 1처럼 공백 두 칸인데, 제품 코드(loop_lock.sh)는 공백을 한 칸으로 줄여 비교한다. 매달 1후속
test_a8마커의 공백 정리(테스트 한 줄).EnvFileError를 트레이스백 대신ERROR:한 줄로(claude S1).MESSAGING.md의 "없는MAM_ENV_FILE은 오류" 설명이 실제(건너뜀)와 다름(grok).MAM_WAIT_GRACE_S·MAM_CHILD_WAIT_S·MAM_JOB_TREE_MAX를_positive_int로(grok);[ -ge ]전용 노브의 비숫자 값(claude R3);mam_int·플래그 검증을 #17 항목 4 이전 목록에(claude R5).🤖 Generated with Claude Code