Skip to content

Commit f4e6eeb

Browse files
committed
fix: launch leased-continuation CLI through the selected test Python
Review follow-up. The new CLI test started `promisify(execFile)("python3", …)`, so the bare system interpreter (Python 3.9 on this host) ran `loopx.cli` and failed on `@dataclass(slots=True)`; only a PATH-injected `uv run` hid it. The merged checkout guard in `test_python_runtime.test.ts` also missed the curried call shape, so it let the regression through. Both subprocesses now take their interpreter from `scripts/test-python.mjs`, and the guard sees through promisified and aliased launchers while still ignoring a resolved interpreter path or an unrelated helper argument. Verified with the system Python 3.9 in PATH and no `uv` wrapper: the bare `node --experimental-strip-types --test` run of the new suite reports 10 passed, 1 skipped (PostgreSQL), and restoring the literal `python3` launch makes the guard name the file. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent 3a1be5a commit f4e6eeb

2 files changed

Lines changed: 26 additions & 3 deletions

File tree

‎tests/control_plane_ts/leased_continuation.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import {executeCoordinationTodoUpdate} from "../../loopx/control_plane/coordinat
1919
import {indexCoordinationProjection, prepareCoordinationProjectionCommit} from "../../loopx/control_plane/coordination/coordination_projection.ts";
2020
import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts";
2121
import {productionScaleCoordinationFixture, PRODUCTION_SCALE_VALIDATION_DECLARATION} from "./production_scale_coordination_fixture.ts";
22+
import {resolveTestPython} from "../../scripts/test-python.mjs";
23+
24+
// The CLI under test is the source checkout's own interpreter, never a bare
25+
// `python3` alias that may be an incompatible system interpreter.
26+
const executeFile = promisify(execFile);
27+
const PYTHON = resolveTestPython();
2228

2329
async function loaded(store: AuthorityStore) {
2430
const head = await store.loadAuthority();
@@ -193,7 +199,7 @@ for (const provider of ["file", "sqlite"]) test(`${provider}: public CLI deliver
193199
expected_shadow_provider_revision: "fixture"}});
194200
assert.equal(fence.status, "applied", JSON.stringify(fence));
195201
const run = async (args: string[]) => {
196-
const {stdout} = await promisify(execFile)("python3", ["-m", "loopx.cli", "--registry", registry,
202+
const {stdout} = await executeFile(PYTHON, ["-m", "loopx.cli", "--registry", registry,
197203
"--runtime-root", f.root, "--format", "json", ...args],
198204
{env: {...process.env, PYTHONPATH: process.cwd()}, timeout: 60000, maxBuffer: 4 * 1024 * 1024});
199205
return JSON.parse(stdout);
@@ -221,7 +227,7 @@ for (const provider of ["file", "sqlite"]) test(`${provider}: public CLI deliver
221227
assert.equal((await loaded(f.store)).provider_revision, committed.provider_revision);
222228
assert.ok(["delivered", "current"].includes(prepared.projection_delivery), JSON.stringify(prepared));
223229
const projected = async () => {
224-
const {stdout} = await promisify(execFile)("python3", ["-c",
230+
const {stdout} = await executeFile(PYTHON, ["-c",
225231
"import json,sys; from pathlib import Path; from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source; rows=parse_todo_source(Path(sys.argv[1]).read_text())[0]['agent']; print(json.dumps(next(r for r in rows if r['todo_id']==sys.argv[2])))",
226232
state, f.base.todo_id], {env: {...process.env, PYTHONPATH: process.cwd()}});
227233
return JSON.parse(stdout);

‎tests/control_plane_ts/test_python_runtime.test.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,25 @@ test("test and browser smokes may not introduce bare python or python3 subproces
8181
// incompatible interpreter), so direct launches, fallbacks and assigned
8282
// defaults must both route through resolveTestPython().
8383
const direct = /\b(?:spawn|spawnSync|execFile|execFileSync)\s*\(\s*["']python3?["']/;
84+
// A promisified or aliased launcher starts the same bare alias, so the guard
85+
// must see through the wrapper instead of accepting the indirection.
86+
const launchers = "spawn|spawnSync|execFile|execFileSync";
87+
const promisified = new RegExp(`\\b(?:promisify|util\\s*\\.\\s*promisify)\\s*\\(\\s*(?:${launchers})\\s*\\)\\s*\\(\\s*["']python3?["']`);
88+
const aliasBinding = new RegExp(`\\b(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:promisify|util\\s*\\.\\s*promisify)\\s*\\(\\s*(?:${launchers})\\s*\\)`, "g");
89+
const aliasedLaunch = (source: string): boolean => [...source.matchAll(aliasBinding)]
90+
.map(match => new RegExp(`\\b${match[1]}\\s*\\(\\s*["']python3?["']`))
91+
.some(pattern => pattern.test(source));
8492
const fallback = /(?:\?\?|\|\|)\s*["']python3?["']/;
8593
const assigned = /\b(?:const|let)\s+\w+\s*=\s*["']python3?["']/;
8694
const bare = JSON.stringify("python3");
8795
const barePython = JSON.stringify("python");
8896
assert.ok(direct.test(`spawn(${bare}, ["-m", "loopx.cli"])`));
8997
assert.ok(direct.test(`spawn(${barePython}, ["-m", "loopx.cli"])`));
9098
assert.ok(direct.test(`spawnSync(${barePython}, ["-c", "raise SystemExit(0)"])`));
99+
assert.ok(promisified.test(`promisify(execFile)(${bare}, ["-m", "loopx.cli"])`));
100+
assert.ok(promisified.test(`util.promisify(execFileSync)(${barePython}, [])`));
101+
assert.ok(aliasedLaunch(`const run = promisify(execFile);\nrun(${bare}, ["-m", "loopx.cli"]);`));
102+
assert.ok(aliasedLaunch(`const run = util.promisify(spawnSync);\nawait run(${barePython}, []);`));
91103
assert.ok(fallback.test(`process.env.LOOPX_TEST_PYTHON ?? ${bare}`));
92104
assert.ok(fallback.test(`process.env.LOOPX_TEST_PYTHON ?? ${barePython}`));
93105
assert.ok(fallback.test(`process.env.NEW_TEST_PYTHON || ${bare}`));
@@ -97,6 +109,10 @@ test("test and browser smokes may not introduce bare python or python3 subproces
97109
assert.ok(assigned.test(`const testInterpreter = ${bare}`));
98110
assert.equal(direct.test(`validation_command_argv: [${bare}, "-m", "pytest"]`), false);
99111
assert.equal(direct.test(`validation_command_argv: [${barePython}, "-m", "pytest"]`), false);
112+
assert.equal(promisified.test(`promisify(execFile)(${JSON.stringify("/usr/bin/python3")}, [])`), false);
113+
// A resolved interpreter or an unrelated helper argument is not a launch.
114+
assert.equal(aliasedLaunch(`const run = promisify(execFile);\nrun(PYTHON, ["-m", "loopx.cli"]);`), false);
115+
assert.equal(direct.test(`qualificationHelperArgv(${bare})`), false);
100116
// An absolute path or a versioned executable is a resolved interpreter, not a bare alias.
101117
assert.equal(direct.test(`spawnSync(${JSON.stringify("/usr/bin/python3")}, [])`), false);
102118
assert.equal(assigned.test(`const executable = ${JSON.stringify("/opt/loopx-qualification/bin/python")}`), false);
@@ -106,7 +122,8 @@ test("test and browser smokes may not introduce bare python or python3 subproces
106122
if (entry.isDirectory()) inspect(path);
107123
else if (/\.(?:cjs|js|mjs|mts|ts)$/.test(entry.name)) {
108124
const source = readFileSync(join(root, path), "utf8");
109-
if (direct.test(source) || fallback.test(source) || assigned.test(source)) offenders.push(path);
125+
if (direct.test(source) || promisified.test(source) || aliasedLaunch(source)
126+
|| fallback.test(source) || assigned.test(source)) offenders.push(path);
110127
}
111128
}
112129
}

0 commit comments

Comments
 (0)