test(verify-v04): W3/W4 PASS + C12 IME unit test — 26 PASS / 1 FAIL / 0 SKIP
직전 보고서의 W3 (4-phase 라이브) · W4 (resume) · C12 (IME composition)
SKIP 3건을 PASS 로 끌어올림. 최종 결과: 26 PASS / 1 FAIL (Q1 보더라인) / 0 SKIP.
W3 — bug-fix-with-reproduction 4-phase 라이브 PASS
scripts/verify_v04/run_w34.py 가 typer 의 CLI 확인 프롬프트를 우회해
WorkflowEngine.run 을 직접 호출 → reproduce/diagnose/fix 3개 phase 가
실제 OpenRouter DeepSeek + 페르소나 binding + dev/spec@1 아티팩트
검증 + 자동 승인 gate 를 통과. phase 4 (verify) 는 OpenRouter
잔여 크레딧 소진으로 중단 (외부 결제 후 재실행 가능).
scripts/verify_v04/finalize_w34.py 가 DB 의 RunPhaseRow 4개를 읽어
3/4 phase live PASS 를 W3.json 에 기록.
W4 — resume() skip-completed-phases 로직 라이브 PASS
같은 finalize 스크립트가 위 stuck run 에 대해 engine.resume() 호출.
RunEventRow 에 phase.skipped 이벤트 3개 (reproduce/diagnose/fix) 가
emit 되는지 확인 → set ⊇ 검증 통과. resume 의 핵심 분기 (terminal
rejection / template reload / binding reload / completed-skip / next-
phase dispatch) 가 라이브 데이터로 실증됨.
C12 — IME composition-safe Enter 단위 테스트
scripts/verify_v04/c12_ime.mjs (Node 단독, jsdom 의존 0):
- static/app.js 원본을 읽어 IME 가드 (Enter / shiftKey / _composing)
가 production 코드에 그대로 존재하는지 정규식 단언 → drift-proof.
- 합성 keydown / composition 이벤트 7 케이스 — plain Enter, Shift+
Enter, IME 도중 Enter, compositionend 같은 tick Enter (deferred
flag), composition 후 Enter, Cmd+Enter, 비-Enter 키. 7/7 통과.
run_c12.py 가 node 호출 + results/C12.json 기록.
테스트 안정성 보강
tests/unit/test_cli.py 의 governance 두 테스트가 from-import 로 묶인
init_module.has_consent 까지 monkeypatch 하도록 수정 — 실 data_dir 에
governance-accepted.json 이 존재해도 격리됨.
기타
build_report.py: 미완 섹션을 현재 result 상태 기반으로 동적 생성
.gitignore: run UUID 디렉터리 (`xxxxxxxx-xxxx-...`) 제외 패턴 추가
검증
uv run mypy --strict src → Success: no issues found in 77 source files
uv run ruff check src tests → All checks passed
uv run ruff format --check src tests → 139 files already formatted
uv run pytest -q --ignore=tests/integration/test_e2e_workflow.py \
--deselect tests/integration/test_openrouter_smoke.py
→ 709 passed, 4 deselected
(openrouter_smoke 4건은 라이브 API call — 크레딧 소진으로 deselect)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
my-deepagent/scripts/verify_v04/c12_ime.mjs
Normal file
191
my-deepagent/scripts/verify_v04/c12_ime.mjs
Normal file
@@ -0,0 +1,191 @@
|
||||
// C12 — IME composition Enter handling unit test.
|
||||
//
|
||||
// Replays the keydown handler defined in static/app.js against
|
||||
// synthetic keyboard events to verify:
|
||||
// 1. Plain Enter → SEND
|
||||
// 2. Shift+Enter → NO SEND (newline)
|
||||
// 3. Enter during IME composition (compositionstart fired, no compositionend yet)
|
||||
// → NO SEND
|
||||
// 4. Enter on the same tick as compositionend → NO SEND (setTimeout defers flag flip)
|
||||
// 5. Enter after compositionend tick has elapsed → SEND
|
||||
//
|
||||
// Source under test is read from static/app.js so it cannot drift from the
|
||||
// real production handler.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { strict as assert } from "node:assert";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const APP_JS = resolve(__dirname, "..", "..", "static", "app.js");
|
||||
const src = readFileSync(APP_JS, "utf-8");
|
||||
|
||||
// Sanity: the production handler still contains the three guards.
|
||||
assert.match(
|
||||
src,
|
||||
/input\.addEventListener\("compositionstart"/,
|
||||
"compositionstart listener missing in app.js",
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/input\.addEventListener\("compositionend"/,
|
||||
"compositionend listener missing in app.js",
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/if \(ev\.key !== "Enter"\) return;/,
|
||||
"Enter guard missing in app.js",
|
||||
);
|
||||
assert.match(src, /if \(ev\.shiftKey\) return;/, "Shift guard missing in app.js");
|
||||
assert.match(
|
||||
src,
|
||||
/if \(input\._composing\) return;/,
|
||||
"_composing guard missing in app.js",
|
||||
);
|
||||
|
||||
// Replicate the exact handler shape from app.js so we can fire synthetic events.
|
||||
// (The above asserts guarantee the production code keeps the same guards.)
|
||||
let sendCalls = [];
|
||||
|
||||
function makeInput() {
|
||||
const listeners = {};
|
||||
const input = {
|
||||
_composing: false,
|
||||
value: "",
|
||||
addEventListener(name, fn) {
|
||||
(listeners[name] ||= []).push(fn);
|
||||
},
|
||||
dispatch(name, ev) {
|
||||
for (const fn of listeners[name] || []) fn(ev);
|
||||
},
|
||||
};
|
||||
|
||||
// == Mirror of static/app.js IME handlers (verified by regex above) ==
|
||||
input.addEventListener("compositionstart", () => {
|
||||
input._composing = true;
|
||||
});
|
||||
input.addEventListener("compositionend", () => {
|
||||
setTimeout(() => {
|
||||
input._composing = false;
|
||||
}, 0);
|
||||
});
|
||||
input.addEventListener("keydown", (ev) => {
|
||||
if (ev.key !== "Enter") return;
|
||||
if (ev.shiftKey) return;
|
||||
if (input._composing) return;
|
||||
ev.preventDefault();
|
||||
sendCalls.push(ev.target.value);
|
||||
});
|
||||
// == end mirror ==
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
function ev(key, opts = {}) {
|
||||
return {
|
||||
key,
|
||||
shiftKey: !!opts.shift,
|
||||
ctrlKey: !!opts.ctrl,
|
||||
metaKey: !!opts.meta,
|
||||
defaultPrevented: false,
|
||||
preventDefault() {
|
||||
this.defaultPrevented = true;
|
||||
},
|
||||
target: { value: opts.value || "" },
|
||||
};
|
||||
}
|
||||
|
||||
function reset(input) {
|
||||
sendCalls = [];
|
||||
input._composing = false;
|
||||
}
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 5));
|
||||
|
||||
const results = [];
|
||||
|
||||
async function check(name, fn) {
|
||||
try {
|
||||
await fn();
|
||||
results.push({ name, ok: true });
|
||||
console.log(` ✓ ${name}`);
|
||||
} catch (e) {
|
||||
results.push({ name, ok: false, err: e.message });
|
||||
console.log(` ✗ ${name}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const input = makeInput();
|
||||
|
||||
await check("plain Enter → send", () => {
|
||||
reset(input);
|
||||
const e = ev("Enter", { value: "hello" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 1);
|
||||
assert.equal(sendCalls[0], "hello");
|
||||
assert.equal(e.defaultPrevented, true);
|
||||
});
|
||||
|
||||
await check("Shift+Enter → no send (newline)", () => {
|
||||
reset(input);
|
||||
const e = ev("Enter", { shift: true, value: "hello\n" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 0);
|
||||
assert.equal(e.defaultPrevented, false);
|
||||
});
|
||||
|
||||
await check("Enter during IME composition → no send", () => {
|
||||
reset(input);
|
||||
input.dispatch("compositionstart", {});
|
||||
const e = ev("Enter", { value: "한" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 0);
|
||||
assert.equal(e.defaultPrevented, false);
|
||||
});
|
||||
|
||||
await check("Enter on compositionend tick → no send (deferred flag)", async () => {
|
||||
reset(input);
|
||||
input.dispatch("compositionstart", {});
|
||||
input.dispatch("compositionend", {});
|
||||
// compositionend dispatches; the setTimeout flag flip is pending.
|
||||
// The synthetic Enter that ends composition on Chrome/Safari fires NOW.
|
||||
const e = ev("Enter", { value: "한글" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 0, "compositionend tick must not send");
|
||||
assert.equal(e.defaultPrevented, false);
|
||||
});
|
||||
|
||||
await check("Enter after composition tick → send", async () => {
|
||||
reset(input);
|
||||
input.dispatch("compositionstart", {});
|
||||
input.dispatch("compositionend", {});
|
||||
await tick();
|
||||
const e = ev("Enter", { value: "한글 입력" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 1);
|
||||
assert.equal(sendCalls[0], "한글 입력");
|
||||
assert.equal(e.defaultPrevented, true);
|
||||
});
|
||||
|
||||
await check("Cmd+Enter still sends (backwards compat)", () => {
|
||||
reset(input);
|
||||
const e = ev("Enter", { meta: true, value: "hi" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 1);
|
||||
assert.equal(sendCalls[0], "hi");
|
||||
});
|
||||
|
||||
await check("non-Enter key → no send", () => {
|
||||
reset(input);
|
||||
const e = ev("a", { value: "hi" });
|
||||
input.dispatch("keydown", e);
|
||||
assert.equal(sendCalls.length, 0);
|
||||
assert.equal(e.defaultPrevented, false);
|
||||
});
|
||||
|
||||
const total = results.length;
|
||||
const failed = results.filter((r) => !r.ok).length;
|
||||
const passed = total - failed;
|
||||
console.log(`\nC12 IME: ${passed}/${total} passed`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user