From 7a03b63382d1421a4fb04e3c2ade5fedc7c52016 Mon Sep 17 00:00:00 2001 From: lATAl Date: Sun, 13 Sep 2026 18:56:52 +0700 Subject: [PATCH] fix(bridge): accept null project values in umbrella handshakes --- packages/extension/src/protocol/stdio.ts | 2 +- packages/extension/src/protocol/types.ts | 2 +- .../integration/extension-stdio-smoke.test.ts | 29 +++++++++++++++++++ .../extension/test/protocol-stdio.test.ts | 7 +++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/protocol/stdio.ts b/packages/extension/src/protocol/stdio.ts index 7f27917f..84ae75e5 100644 --- a/packages/extension/src/protocol/stdio.ts +++ b/packages/extension/src/protocol/stdio.ts @@ -6,7 +6,7 @@ import type { StdioMessage } from './types.ts' const openObject = { additionalProperties: true } as const const bridgeInfoSchema = Type.Object( { - project: Type.Optional(Type.String()), + project: Type.Optional(Type.Union([Type.String(), Type.Null()])), version: Type.Optional(Type.String()), build: Type.Optional(Type.String()), protocol: Type.Optional(Type.Number()), diff --git a/packages/extension/src/protocol/types.ts b/packages/extension/src/protocol/types.ts index 84cf9f5a..dce27b48 100644 --- a/packages/extension/src/protocol/types.ts +++ b/packages/extension/src/protocol/types.ts @@ -21,7 +21,7 @@ export interface PendingToolCall { } export interface BridgeInfo { - project?: string + project?: string | null version?: string build?: string protocol?: number diff --git a/packages/extension/test/integration/extension-stdio-smoke.test.ts b/packages/extension/test/integration/extension-stdio-smoke.test.ts index e92bcbe6..f4c6cc9a 100644 --- a/packages/extension/test/integration/extension-stdio-smoke.test.ts +++ b/packages/extension/test/integration/extension-stdio-smoke.test.ts @@ -1,5 +1,6 @@ import { execSync } from 'node:child_process' import * as fs from 'node:fs' +import { tmpdir } from 'node:os' import path from 'node:path' import { getUnavailableReason } from '#src/connection/status.ts' @@ -134,6 +135,34 @@ describe.skipIf(!elixirAvailable || !projectAvailable)( expect(info?.plugins?.map((plugin) => plugin.name)).toContain('DemoPlugin') }) + it('starts an umbrella bridge and serves AST requests without an application name', async () => { + const cwd = fs.mkdtempSync(path.join(tmpdir(), 'pi-elixir-umbrella-')) + + try { + fs.mkdirSync(path.join(cwd, 'apps')) + fs.writeFileSync( + path.join(cwd, 'mix.exs'), + 'defmodule Umbrella.MixProject do\n use Mix.Project\n def project, do: [apps_path: "apps"]\nend\n' + ) + fs.copyFileSync(path.join(cwd, 'mix.exs'), path.join(cwd, 'fixture.ex')) + startEmbeddedInBackground(cwd) + await waitForReady(cwd) + + expect(getBridgeInfo(cwd)?.project).toBeNull() + const result = await callEmbeddedTool(cwd, 'ex_ast_search', { + pattern: 'defmodule _ do _ end', + path: 'fixture.ex' + }) + + expect(result.isError).toBe(false) + expect(structuredPayload(result).kind).toBe('ast_search') + expect(result.text).toContain('Umbrella.MixProject') + } finally { + stopEmbedded(cwd) + fs.rmSync(cwd, { recursive: true, force: true }) + } + }) + it('routes Pi.LLM.complete through the extension request handler', async () => { const result = await callEmbeddedTool(PROJECT_DIR, 'project_eval', { target: 'bridge', diff --git a/packages/extension/test/protocol-stdio.test.ts b/packages/extension/test/protocol-stdio.test.ts index d6c2585e..f9726eac 100644 --- a/packages/extension/test/protocol-stdio.test.ts +++ b/packages/extension/test/protocol-stdio.test.ts @@ -22,7 +22,14 @@ describe('decodeStdioMessage', () => { }) }) + it('accepts a ready message without an umbrella application name', () => { + const message = { type: 'ready', info: { project: null } } + + expect(decodeStdioMessage(message)).toEqual(message) + }) + it('rejects malformed known messages instead of casting them', () => { + expect(decodeStdioMessage({ type: 'ready', info: { project: 42 } })).toBeNull() expect(decodeStdioMessage({ type: 'result', id: '1', text: '42', isError: false })).toBeNull() expect( decodeStdioMessage({