merge: sync with main
This commit is contained in:
commit
2da97b2382
4 changed files with 103 additions and 27 deletions
|
|
@ -249,6 +249,8 @@ console.log("\n=== renderMetricsView ===");
|
|||
toolCalls: 15,
|
||||
assistantMessages: 10,
|
||||
userMessages: 5,
|
||||
totalTruncationSections: 0,
|
||||
continueHereFiredCount: 0,
|
||||
},
|
||||
byPhase: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ import * as fs from "node:fs";
|
|||
import * as path from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
|
||||
const SWIFT_SRC = path.join(__dirname, "speech-recognizer.swift");
|
||||
const RECOGNIZER_BIN = path.join(__dirname, "speech-recognizer");
|
||||
const PYTHON_SCRIPT = path.join(__dirname, "speech-recognizer.py");
|
||||
const __extensionDir = import.meta.dirname!;
|
||||
const SWIFT_SRC = path.join(__extensionDir, "speech-recognizer.swift");
|
||||
const RECOGNIZER_BIN = path.join(__extensionDir, "speech-recognizer");
|
||||
const PYTHON_SCRIPT = path.join(__extensionDir, "speech-recognizer.py");
|
||||
|
||||
const IS_DARWIN = process.platform === "darwin";
|
||||
const IS_LINUX = process.platform === "linux";
|
||||
|
|
|
|||
67
src/tests/extension-smoke.test.ts
Normal file
67
src/tests/extension-smoke.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Extension Smoke Tests
|
||||
*
|
||||
* Verifies every bundled extension can be imported without throwing.
|
||||
* Catches missing imports, circular dependencies, bad top-level code,
|
||||
* and module resolution failures that tsc alone cannot detect (since
|
||||
* extensions are loaded at runtime via jiti, not compiled by tsc).
|
||||
*
|
||||
* This test dynamically discovers all extension entry points using the
|
||||
* same discovery logic as the loader, so new extensions are automatically
|
||||
* covered without updating this file.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const projectRoot = join(fileURLToPath(import.meta.url), "..", "..", "..");
|
||||
const extensionsDir = join(projectRoot, "src", "resources", "extensions");
|
||||
|
||||
// Extensions that can't be smoke-tested in a plain Node process.
|
||||
// Each entry documents WHY so we can remove it when the underlying issue is fixed.
|
||||
const SKIP_EXTENSIONS = new Set([
|
||||
// core.js is a pre-compiled file (no .ts source) — the resolve-ts test hook
|
||||
// rewrites .js→.ts imports and fails because core.ts doesn't exist.
|
||||
// Works fine at runtime via jiti which loads core.js directly.
|
||||
"browser-tools",
|
||||
]);
|
||||
|
||||
test("all bundled extensions can be imported without throwing", async () => {
|
||||
const { discoverExtensionEntryPaths } = await import("../resource-loader.ts");
|
||||
const entryPaths = discoverExtensionEntryPaths(extensionsDir);
|
||||
|
||||
assert.ok(entryPaths.length >= 10, `expected >=10 extensions, found ${entryPaths.length}`);
|
||||
|
||||
const failures: { path: string; error: string }[] = [];
|
||||
let skipped = 0;
|
||||
|
||||
for (const entryPath of entryPaths) {
|
||||
const relPath = entryPath.slice(extensionsDir.length + 1);
|
||||
const extName = relPath.split(/[/\\]/)[0].replace(/\.ts$/, "");
|
||||
|
||||
if (SKIP_EXTENSIONS.has(extName)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await import(pathToFileURL(entryPath).href);
|
||||
} catch (err) {
|
||||
failures.push({
|
||||
path: relPath,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
const report = failures
|
||||
.map((f) => ` ${f.path}: ${f.error}`)
|
||||
.join("\n");
|
||||
assert.fail(
|
||||
`${failures.length}/${entryPaths.length - skipped} extensions failed to import:\n${report}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { AuthStorage, ModelRegistry } from "../../packages/pi-coding-agent/src/index.js";
|
||||
import googleSearchExtension from "../resources/extensions/google-search/index.ts";
|
||||
|
||||
function createMockPI() {
|
||||
const handlers: any[] = [];
|
||||
const notifications: any[] = [];
|
||||
let registeredTool: any = null;
|
||||
|
||||
return {
|
||||
handlers,
|
||||
notifications,
|
||||
registeredTool,
|
||||
on(event: string, handler: any) {
|
||||
handlers.push({ event, handler });
|
||||
|
|
@ -28,11 +25,23 @@ function createMockPI() {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a mock modelRegistry whose getApiKeyForProvider returns the given
|
||||
* JSON string (matching what the real OAuth provider's getApiKey produces).
|
||||
*/
|
||||
function mockModelRegistry(oauthJson?: string) {
|
||||
return {
|
||||
authStorage: {
|
||||
hasAuth: async (_id: string) => !!oauthJson,
|
||||
},
|
||||
getApiKeyForProvider: async (_provider: string) => oauthJson,
|
||||
};
|
||||
}
|
||||
|
||||
test("fix: google-search uses OAuth if GEMINI_API_KEY is missing", async () => {
|
||||
const originalKey = process.env.GEMINI_API_KEY;
|
||||
delete process.env.GEMINI_API_KEY;
|
||||
|
||||
// Mock fetch
|
||||
const originalFetch = global.fetch;
|
||||
(global as any).fetch = async (url: string, options: any) => {
|
||||
assert.ok(url.includes("cloudcode-pa.googleapis.com"), "Should use Cloud Code Assist endpoint");
|
||||
|
|
@ -43,23 +52,29 @@ test("fix: google-search uses OAuth if GEMINI_API_KEY is missing", async () => {
|
|||
response: {
|
||||
candidates: [{ content: { parts: [{ text: "Mocked AI Answer" }] } }]
|
||||
}
|
||||
})
|
||||
}),
|
||||
text: async () => JSON.stringify({
|
||||
response: {
|
||||
candidates: [{ content: { parts: [{ text: "Mocked AI Answer" }] } }]
|
||||
}
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const pi = createMockPI();
|
||||
googleSearchExtension(pi as any);
|
||||
const authStorage = AuthStorage.inMemory({
|
||||
"google-gemini-cli": { type: "oauth", access: "mock-token", projectId: "mock-project" }
|
||||
});
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
const mockCtx = { ui: { notify() {} }, modelRegistry };
|
||||
|
||||
const oauthJson = JSON.stringify({ token: "mock-token", projectId: "mock-project" });
|
||||
const mockCtx = {
|
||||
ui: { notify() {} },
|
||||
modelRegistry: mockModelRegistry(oauthJson),
|
||||
};
|
||||
|
||||
await pi.fire("session_start", {}, mockCtx);
|
||||
const registeredTool = (pi as any).registeredTool;
|
||||
const result = await registeredTool.execute("call-1", { query: "test" }, new AbortController().signal, () => {}, mockCtx);
|
||||
|
||||
|
||||
assert.equal(result.isError, undefined);
|
||||
assert.ok(result.content[0].text.includes("Mocked AI Answer"));
|
||||
} finally {
|
||||
|
|
@ -75,12 +90,11 @@ test("google-search warns if NO authentication is present", async () => {
|
|||
try {
|
||||
const pi = createMockPI();
|
||||
googleSearchExtension(pi as any);
|
||||
const authStorage = AuthStorage.inMemory({}); // No OAuth
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
const notifications: any[] = [];
|
||||
const mockCtx = {
|
||||
ui: { notify(msg: string, level: string) { notifications.push({ msg, level }); } },
|
||||
modelRegistry
|
||||
modelRegistry: mockModelRegistry(undefined),
|
||||
};
|
||||
|
||||
await pi.fire("session_start", {}, mockCtx);
|
||||
|
|
@ -102,23 +116,15 @@ test("google-search uses GEMINI_API_KEY if present (precedence)", async () => {
|
|||
try {
|
||||
const pi = createMockPI();
|
||||
googleSearchExtension(pi as any);
|
||||
|
||||
// Even if OAuth is available, it should prefer the API Key
|
||||
const authStorage = AuthStorage.inMemory({
|
||||
"google-gemini-cli": { type: "oauth", access: "should-not-be-used", projectId: "mock-project" }
|
||||
});
|
||||
const modelRegistry = new ModelRegistry(authStorage);
|
||||
|
||||
const notifications: any[] = [];
|
||||
const mockCtx = {
|
||||
ui: { notify(msg: string, level: string) { notifications.push({ msg, level }); } },
|
||||
modelRegistry
|
||||
modelRegistry: mockModelRegistry(JSON.stringify({ token: "should-not-be-used", projectId: "mock-project" })),
|
||||
};
|
||||
|
||||
await pi.fire("session_start", {}, mockCtx);
|
||||
assert.equal(notifications.length, 0, "Should NOT notify if API Key is present");
|
||||
|
||||
// We don't easily mock the @google/genai client here without more effort,
|
||||
// but we've verified the logic branches.
|
||||
} finally {
|
||||
delete process.env.GEMINI_API_KEY;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue