FN-6808: handle AI CLI probe spawn failures

Resolve synchronous AI CLI probe launch failures as unavailable auth states.

- Catch synchronous spawn failures in Claude and Droid probe helpers so fire-and-forget validation paths do not reject.
- Return unavailable/unauthenticated sentinels for Droid model discovery and CLI presence/auth checks.
- Add regression coverage for Claude and Droid presence/auth probes plus a patch changeset.

Files changed:
 .../fn-6808-cli-probe-unhandled-rejection.md       |  5 ++++
 .../src/__tests__/process-manager.test.ts          | 25 ++++++++++++++++++++
 .../src/__tests__/process-manager.test.ts          | 25 ++++++++++++++++++++
 packages/pi-claude-cli/src/process-manager.ts      | 12 +++++++++-
 .../src/process-manager.ts                         | 27 +++++++++++++++++++---
 5 files changed, 90 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-6808

Fusion-Task-Lineage: 79702ef2-e116-4313-a557-da17a6888c30
This commit is contained in:
gsxdsm
2026-06-20 21:30:53 -07:00
parent 185ff70d86
commit 37c4cfa56e
5 changed files with 90 additions and 4 deletions

View File

@@ -219,10 +219,20 @@ export function captureStderr(proc: ChildProcess): () => string {
* does this on every chat send), sync probes freeze every other request.
* This async variant uses spawn so the loop keeps turning while the subprocess
* starts up.
*
* FNXC:CliRuntime 2026-06-20-17:25:
* FN-6808/FN-6801 require this fire-and-forget auth/presence probe to never reject. Catch synchronous spawn throws from the Vitest child-process guard or platform launch errors and resolve 127, matching the async error sentinel so callers degrade to unauthenticated/not-present instead of surfacing unhandled promise rejections.
*/
function runDroidProbe(args: string[], timeoutMs = 45000): Promise<number> {
return new Promise((resolve) => {
const proc = spawn("droid", args, { stdio: "ignore" });
let proc: ChildProcess;
try {
proc = spawn("droid", args, { stdio: "ignore" });
} catch {
resolve(127);
return;
}
const timer = setTimeout(() => {
try {
proc.kill("SIGKILL");
@@ -275,11 +285,22 @@ export async function validateCliAuthAsync(): Promise<boolean> {
}
export async function discoverDroidModels(): Promise<string[]> {
const attempts: string[][] = [["models", "--json"], ["model", "list", "--json"], ["models"]];
const attempts: string[][] = [
["models", "--json"],
["model", "list", "--json"],
["models"],
];
for (const args of attempts) {
const models = await new Promise<string[] | null>((resolve) => {
const proc = spawn("droid", args, { stdio: ["ignore", "pipe", "ignore"] });
let proc: ChildProcess;
try {
proc = spawn("droid", args, { stdio: ["ignore", "pipe", "ignore"] });
} catch {
resolve(null);
return;
}
let out = "";
proc.stdout?.on("data", (chunk: Buffer) => {
out += chunk.toString();