Merge remote-tracking branch 'origin/main' into pr-586-r2

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
This commit is contained in:
can1357
2026-04-01 15:33:13 +02:00
55 changed files with 1048 additions and 588 deletions
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { joinPatch } from "@oh-my-pi/pi-coding-agent/commit/git";
describe("joinPatch", () => {
test("preserves space character in empty context line at end of patch", () => {
// This simulates a hunk ending with an empty context line represented as " \n"
const parts = [
"@@ -1,2 +1,2 @@\n",
"foo\n",
"@@ -10,4 +10,4 @@\n",
"line1\n",
"-old\n",
"+new\n",
" \n", // Empty context line = space + newline
];
const result = joinPatch(parts);
// The result should end with a space character (the empty context line)
// but NOT start/end with multiple newlines
expect(result.endsWith(" \n")).toBe(true);
expect(result.replace(/[ \t]+$/, "")).toEqual(result); // No trailing spaces should be removed
});
test("normalizes multiple trailing newlines in parts", () => {
const parts = ["line1\n", "line2\n", "line3"];
const result = joinPatch(parts);
// Should join with single newlines and end with one newline
expect(result.endsWith("\n")).toBe(true);
});
test("adds newline to parts that are missing them", () => {
const parts = ["line1", "line2"];
const result = joinPatch(parts);
// Should add newlines to both parts
expect(result.includes("line1\n")).toBe(true);
expect(result.endsWith("line2\n")).toBe(true);
});
});
@@ -4,6 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { Effort, type OpenAICompat, type ThinkingConfig } from "@oh-my-pi/pi-ai";
import { kNoAuth, MODEL_ROLES, ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { hookFetch, Snowflake } from "@oh-my-pi/pi-utils";
@@ -18,6 +19,7 @@ describe("ModelRegistry", () => {
});
beforeEach(async () => {
_resetSettingsForTest();
tempDir = path.join(os.tmpdir(), `pi-test-model-registry-${Snowflake.next()}`);
fs.mkdirSync(tempDir, { recursive: true });
modelsJsonPath = path.join(tempDir, "models.json");
@@ -25,6 +27,7 @@ describe("ModelRegistry", () => {
});
afterEach(() => {
_resetSettingsForTest();
authStorage.close();
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true });
@@ -1034,6 +1037,38 @@ describe("ModelRegistry", () => {
expect(model.baseUrl).toBe(initialBaseUrl);
});
});
describe("disabled provider filtering", () => {
test("getAvailable and getDiscoverableProviders exclude disabled providers from settings", async () => {
writeRawModelsJson({
ollama: {
baseUrl: "http://127.0.0.1:11434/v1",
api: "openai-completions",
auth: "none",
discovery: { type: "ollama" },
},
});
await authStorage.set("github-copilot", [
{
type: "oauth",
access: "tid=1;proxy-ep=proxy.individual.githubcopilot.com;exp=9999999999",
refresh: "refresh-individual",
expires: Date.now() + 60_000,
},
]);
await Settings.init({
inMemory: true,
overrides: {
disabledProviders: ["github-copilot", "ollama"],
},
});
const registry = new ModelRegistry(authStorage, modelsJsonPath);
expect(registry.getAvailable().some(model => model.provider === "github-copilot")).toBe(false);
expect(registry.getDiscoverableProviders()).not.toContain("ollama");
});
});
describe("runtime discovery", () => {
test("auto-discovers ollama models without provider config", async () => {
using _hook = hookFetch(input => {
@@ -1,39 +0,0 @@
import { describe, expect, it } from "bun:test";
import { hookFetch } from "@oh-my-pi/pi-utils";
import { executeCodeSearch } from "../../src/web/search/code-search";
function getFirstTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
const firstContent = result.content[0];
if (firstContent?.type === "text") return firstContent.text ?? "";
return "";
}
describe("code_search", () => {
it("normalizes Exa response and surfaces it as code search result", async () => {
using _hook = hookFetch(
() =>
new Response(
JSON.stringify({
result: {
content: [
{
type: "text",
text: "Need the official or source-backed way to silence direnv loading output.",
},
],
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
),
);
const result = await executeCodeSearch({ query: "DIRENV_LOG_FORMAT direnv loading silence" });
const output = getFirstTextContent(result);
expect(output).toContain("Code search via exa");
expect(output).toContain("Need the official or source-backed way to silence direnv loading output.");
expect(result.details?.provider).toBe("exa");
expect(result.details?.response?.sources[0]?.snippet).toContain(
"Need the official or source-backed way to silence direnv loading output.",
);
});
});