From e5abf250c534cc1e34aefdbe878297c770c03734 Mon Sep 17 00:00:00 2001 From: mmprotest Date: Mon, 29 Jun 2026 11:55:23 +1000 Subject: [PATCH] Fix Pi release test bridge launch --- integrations/pi-villani/README.md | 2 +- integrations/pi-villani/docs/release.md | 2 +- integrations/pi-villani/src/extension.test.ts | 26 +++++++++++++------ integrations/pi-villani/src/index.ts | 9 ++++++- integrations/pi-villani/src/process.ts | 4 +-- integrations/pi-villani/src/runtimeConfig.ts | 2 +- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/integrations/pi-villani/README.md b/integrations/pi-villani/README.md index 57794a51..e2ae68ec 100644 --- a/integrations/pi-villani/README.md +++ b/integrations/pi-villani/README.md @@ -5,7 +5,7 @@ pi install npm:@mmprotest/pi-villani ``` Provides `/villani `. -Runtime version: `v0.1.3`. +Runtime version: `v0.1.4`. ## Usage diff --git a/integrations/pi-villani/docs/release.md b/integrations/pi-villani/docs/release.md index b15606be..d0f95e8a 100644 --- a/integrations/pi-villani/docs/release.md +++ b/integrations/pi-villani/docs/release.md @@ -1,5 +1,5 @@ # Release checklist -1. Publish matching GitHub runtime release `pi-villani-runtime-v0.1.3`. +1. Publish matching GitHub runtime release `pi-villani-runtime-v0.1.4`. 2. Verify assets and checksums. 3. Publish `@mmprotest/pi-villani`. Install: `pi install npm:@mmprotest/pi-villani`. diff --git a/integrations/pi-villani/src/extension.test.ts b/integrations/pi-villani/src/extension.test.ts index e9a474e3..daceb80e 100644 --- a/integrations/pi-villani/src/extension.test.ts +++ b/integrations/pi-villani/src/extension.test.ts @@ -45,6 +45,12 @@ function bridgeScript(body: string) { } const readyPrelude = "process.stdout.write(JSON.stringify({type:'ready'})+'\\n');\nconst send=e=>process.stdout.write(JSON.stringify(e)+'\\n');\n"; +function fakeBridgeLaunch(fakeBridgeScriptPath: string) { + return { + executableOverride: process.execPath, + bridgeArgsOverride: [fakeBridgeScriptPath], + }; +} test("/villani sends command notification and only reports run started after run_started event", async () => { const old = process.env.VILLANI_COMMAND; @@ -61,6 +67,7 @@ test("/villani sends command notification and only reports run started after run { sendMessage: (m: string) => notes.push(m) }, { ui: { notify: (m: string) => notes.push(m) }, + bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" }, }, @@ -93,7 +100,7 @@ test("/villani missing run_started timeout reports visible error instead of star runVillani( "task", {}, - { ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" }, + { ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" }, ), /did not acknowledge run command within 10 seconds.*no ack/s, ); @@ -145,7 +152,7 @@ setInterval(()=>{},1000); runVillani( "task", {}, - { ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" }, + { ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" }, ), /did not acknowledge run command within 10 seconds.*cleanup no ack/s, ); @@ -168,7 +175,7 @@ test("bridge exit before ready with ModuleNotFoundError produces pip install dia process.env.VILLANI_COMMAND = p; try { await assert.rejects( - () => bridgePing({ ui: { notify: () => {} } }), + () => bridgePing({ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p) }), /pip install -e/, ); } finally { @@ -186,7 +193,7 @@ test("/villani-bridge-ping succeeds with fake bridge", async () => { process.env.VILLANI_COMMAND = p; const notes: string[] = []; try { - await bridgePing({ ui: { notify: (m: string) => notes.push(m) } }); + await bridgePing({ ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p) }); } finally { if (old === undefined) delete process.env.VILLANI_COMMAND; else process.env.VILLANI_COMMAND = old; @@ -200,7 +207,7 @@ test("/villani-bridge-ping surfaces stderr on failure", async () => { process.env.VILLANI_COMMAND = p; try { await assert.rejects( - () => bridgePing({ ui: { notify: () => {} } }), + () => bridgePing({ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p) }), /boom stderr/, ); } finally { @@ -225,7 +232,7 @@ test("/villani surfaces bridge error before run_started without waiting 10 secon runVillani( "task", {}, - { ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" }, + { ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" }, ), /bad run command/, ); @@ -253,7 +260,7 @@ test("/villani launches bridge with ctx cwd", async () => { await runVillani( "task", { sendMessage: () => {} }, - { ui: { notify: () => {} }, cwd: d, model: { id: "m" } }, + { ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: d, model: { id: "m" } }, ); assert.equal(readFileSync(cwdFile, "utf8"), d); } finally { @@ -359,7 +366,7 @@ test("/villani sends exactly one iterable final durable message with summary met await runVillani( "task", { sendMessage: (m: any) => sent.push(m) }, - { ui: { notify: () => {} }, cwd: "/tmp", model: { id: "m" } }, + { ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" } }, ); } finally { if (old === undefined) delete process.env.VILLANI_COMMAND; @@ -448,6 +455,7 @@ test("/villani approval pending clears widget, sets status, confirms, and accept return true; }, }, + bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" }, }, @@ -526,6 +534,7 @@ test("/villani keeps waiting after nonzero tool_finished and renders next model notify: (m: string) => notes.push(m), setStatus: (_: string, m: string) => statuses.push(m), }, + bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" }, }, @@ -632,6 +641,7 @@ test("command lifecycle sets then clears widget through final result", async () setWidget: (...a: any[]) => widgets.push(a), setStatus: () => {}, }, + bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" }, }, diff --git a/integrations/pi-villani/src/index.ts b/integrations/pi-villani/src/index.ts index f1acaf6e..cabdf5e4 100644 --- a/integrations/pi-villani/src/index.ts +++ b/integrations/pi-villani/src/index.ts @@ -5,7 +5,7 @@ import { VILLANI_RUNTIME_TAG, VILLANI_RUNTIME_VERSION, } from "./runtimeConfig.js"; -import { VillaniBridgeProcess } from "./process.js"; +import { VillaniBridgeProcess, type BridgeLaunchOptions } from "./process.js"; import { resolvePiModel, sanitizeError, @@ -159,6 +159,10 @@ function denyPending(run: ActiveRun) { function bridgeStderr(bridge: VillaniBridgeProcess) { return bridge.getRecentStderr?.() ?? bridge.stderr ?? ""; } + +function bridgeLaunchOptions(ctx: any): BridgeLaunchOptions { + return ctx?.bridgeLaunchOptions ?? ctx?.villaniBridgeLaunchOptions ?? {}; +} function bridgeDiagnosticMessage(e: unknown) { const msg = sanitizeError(e); if (/ModuleNotFoundError: No module named ['"]villani_code['"]/.test(msg)) @@ -174,8 +178,10 @@ async function assertBridgePing( ctx: any, env?: NodeJS.ProcessEnv, signal?: AbortSignal, + launchOptions: BridgeLaunchOptions = bridgeLaunchOptions(ctx), ) { const bridge = new VillaniBridgeProcess(executable, { + ...launchOptions, env, startupTimeoutMs: 30000, cwd: ctx.cwd ?? process.cwd(), @@ -319,6 +325,7 @@ export async function runVillani( } await setStatus(ctx, nextVillaniStatus("thinking", "runtime-start") ?? "Villaniplan forming..."); const bridge = new VillaniBridgeProcess(executable, { + ...bridgeLaunchOptions(ctx), env, startupTimeoutMs: 30000, proxyMode: usePi, diff --git a/integrations/pi-villani/src/process.ts b/integrations/pi-villani/src/process.ts index 885a8c7c..f8247d3e 100644 --- a/integrations/pi-villani/src/process.ts +++ b/integrations/pi-villani/src/process.ts @@ -1,8 +1,8 @@ import { spawn, ChildProcessWithoutNullStreams } from 'node:child_process'; import { EventEmitter } from 'node:events'; import type { BridgeEvent } from './protocol.js'; -export interface BridgeProcessOptions{proxyMode?:boolean; explicitConfigMode?:boolean; env?:NodeJS.ProcessEnv; cwd?:string; startupTimeoutMs?:number; bridgeArgs?:string[]} export type LaunchOptions=BridgeProcessOptions; +export interface BridgeLaunchOptions{executableOverride?:string; bridgeArgsOverride?:string[]} export interface BridgeProcessOptions extends BridgeLaunchOptions{proxyMode?:boolean; explicitConfigMode?:boolean; env?:NodeJS.ProcessEnv; cwd?:string; startupTimeoutMs?:number; bridgeArgs?:string[]} export type LaunchOptions=BridgeProcessOptions; export function sanitizedEnv(opts:BridgeProcessOptions={}):NodeJS.ProcessEnv{const e={...(opts.env||process.env)}; if(opts.proxyMode&&!opts.explicitConfigMode){for(const k of Object.keys(e)){if(['OPENAI_API_KEY','ANTHROPIC_API_KEY','VILLANI_API_KEY'].includes(k)||/(_API_KEY|_TOKEN|_AUTH|_BEARER|TOKEN|AUTH|BEARER)$/i.test(k)) delete e[k];}} return e;} export class VillaniBridgeProcess extends EventEmitter{proc:ChildProcessWithoutNullStreams; ready=false; stderr=''; buffer=''; startupTimeoutMs:number; readonly shell=false; finals=new Map(); private exitInfo?:{code:number|null;signal:NodeJS.Signals|null}; - constructor(executable:string, opts:BridgeProcessOptions={}){super(); const args=opts.bridgeArgs??['bridge','--stdio']; this.startupTimeoutMs=opts.startupTimeoutMs??30000; this.proc=spawn(executable,args,{shell:false,env:sanitizedEnv(opts),cwd:opts.cwd}); this.proc.stderr.on('data',d=>{this.stderr=(this.stderr+d.toString()).slice(-8000)}); this.proc.stdout.on('data',d=>this.onout(d.toString())); this.proc.once('error',e=>this.emit('error',e)); this.proc.once('exit',(code,signal)=>{this.exitInfo={code,signal}; this.emit('exit',{code,signal});});} + constructor(executable:string, opts:BridgeProcessOptions={}){super(); const launchExecutable=opts.executableOverride??executable; const args=opts.bridgeArgsOverride??opts.bridgeArgs??['bridge','--stdio']; this.startupTimeoutMs=opts.startupTimeoutMs??30000; this.proc=spawn(launchExecutable,args,{shell:false,env:sanitizedEnv(opts),cwd:opts.cwd}); this.proc.stderr.on('data',d=>{this.stderr=(this.stderr+d.toString()).slice(-8000)}); this.proc.stdout.on('data',d=>this.onout(d.toString())); this.proc.once('error',e=>this.emit('error',e)); this.proc.once('exit',(code,signal)=>{this.exitInfo={code,signal}; this.emit('exit',{code,signal});});} getRecentStderr(){return this.stderr;} onout(s:string){this.buffer+=s; let i; while((i=this.buffer.indexOf('\n'))>=0){const line=this.buffer.slice(0,i); this.buffer=this.buffer.slice(i+1); if(!line.trim())continue; try{const e=JSON.parse(line); if(e.type==='ready')this.ready=true; if(['run_completed','run_failed','run_aborted'].includes(e.type)&&e.id)this.finals.set(e.id,e); this.emit('event',e);}catch{const err=new Error('Malformed JSONL from bridge'); this.kill(); this.emit('error',err);}}} send(c:unknown){try{if(this.proc.stdin.destroyed||!this.proc.stdin.writable)return false; return this.proc.stdin.write(JSON.stringify(c)+'\n');}catch(e){this.emit('error',e); return false;}} abort(runId:string){this.send({type:'abort',id:runId});} respondToApproval(runId:string,requestId:string,approved:boolean){this.send({type:'approval_response',id:runId,request_id:requestId,approved});} kill(){if(!this.proc.killed)this.proc.kill();} diff --git a/integrations/pi-villani/src/runtimeConfig.ts b/integrations/pi-villani/src/runtimeConfig.ts index 0e46ada6..fa3cbcf4 100644 --- a/integrations/pi-villani/src/runtimeConfig.ts +++ b/integrations/pi-villani/src/runtimeConfig.ts @@ -1,4 +1,4 @@ -export const VILLANI_RUNTIME_VERSION = "0.1.3"; +export const VILLANI_RUNTIME_VERSION = "0.1.4"; export const VILLANI_RUNTIME_REPOSITORY = "mmprotest/villani-code"; export const VILLANI_RUNTIME_TAG = `pi-villani-runtime-v${VILLANI_RUNTIME_VERSION}`; export type PlatformKey = "win32-x64"|"darwin-arm64"|"darwin-x64"|"linux-x64";