fix(ai): enforce the feature gate on the per-request fallback path (#331)

The persistent Python dispatcher rejects scripts whose feature bundle is not
installed, but the per-request fallback (used when the dispatcher is down, e.g.
restarting right after a model repair) spawned scripts directly and bypassed
that gate. Behavior was therefore inconsistent: a gated script would fail under
the dispatcher but run under the fallback -- the "works once after a repair"
symptom from the original report.

- add packages/ai/src/feature-gate.ts: SCRIPT_BUNDLE_MAP + missingBundleForScript,
  mirroring TOOL_BUNDLE_MAP in dispatcher.py, reading the same installed.json and
  failing closed exactly like dispatcher._get_installed_bundles()
- runPerRequest now rejects with "feature_not_installed" (the same message the
  dispatcher path surfaces) when a gated script's bundle is not installed
- unit tests for the gate, plus a drift test pinning the TS map to dispatcher.py

Closes #327
This commit is contained in:
SnapOtter
2026-06-22 23:49:41 +08:00
committed by GitHub
parent 32c1192d63
commit 7a70affac5
5 changed files with 166 additions and 2 deletions
+5 -2
View File
@@ -383,7 +383,10 @@ describe("bridge - runPythonWithProgress (per-request fallback)", () => {
const mock = createMockProcess();
vi.mocked(spawn).mockReturnValue(mock.process);
const promise = runPythonWithProgress("remove_bg.py", ["/tmp/in.png", "/tmp/out.png"]);
// Ungated script name: this test exercises generic spawn-args plumbing, not
// a specific bundle. The feature gate on the fallback is covered separately
// in tests/unit/ai/feature-gate.test.ts.
const promise = runPythonWithProgress("test_script.py", ["/tmp/in.png", "/tmp/out.png"]);
mock.stdout.emit("data", Buffer.from('{"success": true}\n'));
mock.emitEvent("close", 0, null);
@@ -402,7 +405,7 @@ describe("bridge - runPythonWithProgress (per-request fallback)", () => {
expect(perRequestCall).toBeDefined();
expect(perRequestCall?.[1]).toEqual(
expect.arrayContaining([
expect.stringContaining("remove_bg.py"),
expect.stringContaining("test_script.py"),
"/tmp/in.png",
"/tmp/out.png",
]),