mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
feat(pi): add first-class Coding Agent support
This commit is contained in:
@@ -112,4 +112,21 @@ describe("ImportExportSection Component", () => {
|
||||
expect(screen.getByText("settings.importFailed")).toBeInTheDocument();
|
||||
expect(screen.getByText("Parse failed")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the Pi portability boundary and the concrete partial-sync failure", () => {
|
||||
render(
|
||||
<ImportExportSection
|
||||
{...baseProps}
|
||||
status="partial-success"
|
||||
errorMessage="unclaimed native key 'occupied' already exists"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("settings.piImportExportBoundary"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("unclaimed native key 'occupied' already exists"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PiNativePromptResources } from "@/components/prompts/PiNativePromptResources";
|
||||
import { promptsApi, type PiPromptFileKind } from "@/lib/api/prompts";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const renderResources = () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<PiNativePromptResources />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe("PiNativePromptResources", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.spyOn(promptsApi, "getPiPromptFile").mockImplementation(
|
||||
async (kind: PiPromptFileKind) => ({
|
||||
kind,
|
||||
path:
|
||||
kind === "system_override"
|
||||
? "/agent/SYSTEM.md"
|
||||
: "/agent/APPEND_SYSTEM.md",
|
||||
exists: kind === "system_append",
|
||||
revision: kind === "system_append" ? "append-revision" : "missing",
|
||||
content: kind === "system_append" ? "append" : "",
|
||||
}),
|
||||
);
|
||||
vi.spyOn(promptsApi, "listPiPromptTemplates").mockResolvedValue([
|
||||
{
|
||||
slug: "empty",
|
||||
content: "",
|
||||
revision: "empty-revision",
|
||||
},
|
||||
]);
|
||||
vi.spyOn(promptsApi, "upsertPiPromptTemplate").mockResolvedValue({
|
||||
slug: "new-empty",
|
||||
content: "",
|
||||
revision: "created-revision",
|
||||
});
|
||||
vi.spyOn(promptsApi, "replacePiPromptFile").mockImplementation(
|
||||
async (kind, _revision, content) => ({
|
||||
kind,
|
||||
path: "/agent/SYSTEM.md",
|
||||
exists: true,
|
||||
revision: "saved-empty",
|
||||
content,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses file presence as state, rejects blank direct saves, and permits empty templates", async () => {
|
||||
renderResources();
|
||||
|
||||
await waitFor(() => expect(screen.getByText("/empty")).toBeInTheDocument());
|
||||
expect(screen.getByText("pi.prompts.active")).toBeInTheDocument();
|
||||
expect(screen.getByText("pi.prompts.inactive")).toBeInTheDocument();
|
||||
|
||||
const instructionEditors = screen.getAllByPlaceholderText(
|
||||
"pi.prompts.instructionPlaceholder",
|
||||
);
|
||||
fireEvent.change(instructionEditors[1], { target: { value: "" } });
|
||||
const saveButtons = screen.getAllByRole("button", { name: "common.save" });
|
||||
expect(saveButtons[1]).toBeDisabled();
|
||||
fireEvent.change(instructionEditors[1], {
|
||||
target: { value: "new append" },
|
||||
});
|
||||
expect(saveButtons[1]).toBeEnabled();
|
||||
fireEvent.click(saveButtons[1]);
|
||||
await waitFor(() =>
|
||||
expect(promptsApi.replacePiPromptFile).toHaveBeenCalledWith(
|
||||
"system_append",
|
||||
"append-revision",
|
||||
"new append",
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("pi.prompts.templateSlug"), {
|
||||
target: { value: "new-empty" },
|
||||
});
|
||||
const create = screen.getByRole("button", {
|
||||
name: "pi.prompts.createTemplate",
|
||||
});
|
||||
expect(create).toBeEnabled();
|
||||
fireEvent.click(create);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(promptsApi.upsertPiPromptTemplate).toHaveBeenCalledWith(
|
||||
"new-empty",
|
||||
"missing",
|
||||
"",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("requires confirmation before creating the dangerous SYSTEM override", async () => {
|
||||
renderResources();
|
||||
|
||||
const instructionEditors = await screen.findAllByPlaceholderText(
|
||||
"pi.prompts.instructionPlaceholder",
|
||||
);
|
||||
fireEvent.change(instructionEditors[0], {
|
||||
target: { value: "replace the system prompt" },
|
||||
});
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: "common.save" })[0],
|
||||
);
|
||||
|
||||
expect(promptsApi.replacePiPromptFile).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.getByText("pi.prompts.activateOverrideTitle"),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "pi.prompts.activateOverride" }),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(promptsApi.replacePiPromptFile).toHaveBeenCalledWith(
|
||||
"system_override",
|
||||
"missing",
|
||||
"replace the system prompt",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PiProviderForm } from "@/components/providers/forms/PiProviderForm";
|
||||
import composerOracle from "../fixtures/pi/native-oracle/composer-oracle-v1.json";
|
||||
|
||||
describe("PiProviderForm", () => {
|
||||
it("submits only explicit model fields and leaves pinned defaults to Pi", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<PiProviderForm
|
||||
appId="pi"
|
||||
submitLabel="Save Pi provider"
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("my-provider"), {
|
||||
target: { value: "verified-provider" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("My Pi provider"), {
|
||||
target: { value: "Verified provider" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("openai-responses"), {
|
||||
target: { value: "openai-responses" },
|
||||
});
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText("https://api.example.com/v1"),
|
||||
{
|
||||
target: { value: "https://api.example.com/v1" },
|
||||
},
|
||||
);
|
||||
fireEvent.change(screen.getByPlaceholderText("model-id"), {
|
||||
target: { value: "opaque-model" },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save Pi provider" }));
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
const submitted = onSubmit.mock.calls[0][0];
|
||||
expect(submitted.providerKey).toBe("verified-provider");
|
||||
expect(JSON.parse(submitted.settingsConfig)).toEqual({
|
||||
name: "Verified provider",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
models: [{ id: "opaque-model" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips every field in the pinned all-fields composer vector", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
const vector = composerOracle.cases.find(
|
||||
(candidate) => candidate.id === "combined-all-fields-precedence",
|
||||
);
|
||||
expect(vector).toBeDefined();
|
||||
const input = JSON.parse(JSON.stringify(vector?.input)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
render(
|
||||
<PiProviderForm
|
||||
appId="pi"
|
||||
providerId="all-fields"
|
||||
submitLabel="Save all fields"
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {}}
|
||||
initialData={{
|
||||
name: String(input.name),
|
||||
settingsConfig: input,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save all fields" }));
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
expect(JSON.parse(onSubmit.mock.calls[0][0].settingsConfig)).toEqual(input);
|
||||
});
|
||||
|
||||
it("preserves an explicitly false authHeader instead of erasing it", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
const input = {
|
||||
name: "Explicit false",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
authHeader: false,
|
||||
models: [{ id: "model" }],
|
||||
};
|
||||
|
||||
render(
|
||||
<PiProviderForm
|
||||
appId="pi"
|
||||
providerId="explicit-false"
|
||||
submitLabel="Save explicit false"
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {}}
|
||||
initialData={{ name: input.name, settingsConfig: input }}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Save explicit false" }),
|
||||
);
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
expect(JSON.parse(onSubmit.mock.calls[0][0].settingsConfig)).toEqual(input);
|
||||
});
|
||||
|
||||
it("creates and submits typed failover endpoints from the real form entry", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<PiProviderForm
|
||||
appId="pi"
|
||||
submitLabel="Save endpoint provider"
|
||||
onSubmit={onSubmit}
|
||||
onCancel={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("my-provider"), {
|
||||
target: { value: "endpoint-provider" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("My Pi provider"), {
|
||||
target: { value: "Endpoint provider" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("openai-responses"), {
|
||||
target: { value: "openai-responses" },
|
||||
});
|
||||
fireEvent.change(screen.getByPlaceholderText("model-id"), {
|
||||
target: { value: "endpoint-model" },
|
||||
});
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "pi.form.manageEndpoints" }),
|
||||
);
|
||||
const endpointInput = await screen.findByPlaceholderText(
|
||||
"endpointTest.addEndpointPlaceholder",
|
||||
);
|
||||
fireEvent.change(endpointInput, {
|
||||
target: { value: "https://failover.example/v1" },
|
||||
});
|
||||
fireEvent.keyDown(endpointInput, { key: "Enter" });
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.save" }));
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Save endpoint provider" }),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
expect(JSON.parse(onSubmit.mock.calls[0][0].settingsConfig)).toMatchObject({
|
||||
baseUrl: "https://failover.example/v1",
|
||||
models: [{ id: "endpoint-model" }],
|
||||
});
|
||||
expect(onSubmit.mock.calls[0][0].meta.custom_endpoints).toEqual({
|
||||
"https://failover.example/v1": expect.objectContaining({
|
||||
url: "https://failover.example/v1",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import PromptPanel from "@/components/prompts/PromptPanel";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
reconcilePiLibrary: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useTauriEvent", () => ({
|
||||
useTauriEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/prompts/PiNativePromptResources", () => ({
|
||||
PiNativePromptResources: () => <div>native resources</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/usePromptActions", () => ({
|
||||
usePromptActions: () => ({
|
||||
prompts: {},
|
||||
loading: false,
|
||||
currentFileContent: "external native content",
|
||||
piLibraryStatus: {
|
||||
nativeExists: true,
|
||||
nativeRevision: "native-revision",
|
||||
matchedPromptId: null,
|
||||
needsReconciliation: true,
|
||||
},
|
||||
reload: mocks.reload,
|
||||
savePrompt: vi.fn(),
|
||||
deletePrompt: vi.fn(),
|
||||
enablePrompt: vi.fn(),
|
||||
toggleEnabled: vi.fn(),
|
||||
importFromFile: vi.fn(),
|
||||
reconcilePiLibrary: mocks.reconcilePiLibrary,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("PromptPanel Pi native reconciliation", () => {
|
||||
beforeEach(() => {
|
||||
mocks.reconcilePiLibrary.mockReset();
|
||||
mocks.reconcilePiLibrary.mockResolvedValue(undefined);
|
||||
mocks.reload.mockReset();
|
||||
});
|
||||
|
||||
it("shows native drift and exposes the explicit reconciliation action", () => {
|
||||
render(<PromptPanel open appId="pi" onOpenChange={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("pi.prompts.libraryDriftTitle"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("pi.prompts.libraryDriftNative"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "pi.prompts.reconcileLibrary" }),
|
||||
);
|
||||
expect(mocks.reconcilePiLibrary).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,13 @@ import { describe, expect, it } from "vitest";
|
||||
import { FAILOVER_APPS } from "@/components/settings/ProxyTabContent";
|
||||
|
||||
describe("ProxyTabContent failover apps", () => {
|
||||
it("exposes Grok Build alongside the existing failover applications", () => {
|
||||
it("exposes Pi alongside the existing failover applications", () => {
|
||||
expect(FAILOVER_APPS.map(({ id }) => id)).toEqual([
|
||||
"claude",
|
||||
"codex",
|
||||
"gemini",
|
||||
"grokbuild",
|
||||
"pi",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
|
||||
import { piApi } from "@/lib/api/pi";
|
||||
import { sessionsApi } from "@/lib/api/sessions";
|
||||
import type { SessionMessage, SessionMeta } from "@/types";
|
||||
import { setSessionFixtures } from "../msw/state";
|
||||
@@ -221,6 +222,23 @@ describe("SessionManagerPage", () => {
|
||||
setSessionFixtures(sessions, messages);
|
||||
});
|
||||
|
||||
it("surfaces a relative Pi sessionDir instead of presenting an empty scan as authoritative", async () => {
|
||||
const discovery = vi
|
||||
.spyOn(piApi, "getSessionDiscovery")
|
||||
.mockResolvedValue({
|
||||
status: "requires_project_context",
|
||||
configuredPath: ".pi/sessions",
|
||||
source: "settings",
|
||||
});
|
||||
|
||||
renderPage("pi");
|
||||
|
||||
const notice = await screen.findByRole("status");
|
||||
expect(notice).toHaveTextContent(".pi/sessions");
|
||||
expect(discovery).toHaveBeenCalledTimes(1);
|
||||
discovery.mockRestore();
|
||||
});
|
||||
|
||||
it("deletes the selected session and selects the next visible session", async () => {
|
||||
renderPage();
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ const importSkillsMock = vi.fn();
|
||||
const installFromZipMock = vi.fn();
|
||||
const deleteSkillBackupMock = vi.fn();
|
||||
const restoreSkillBackupMock = vi.fn();
|
||||
const skillsHookState = vi.hoisted(() => ({
|
||||
installed: [] as unknown[],
|
||||
piStatuses: {} as Record<string, unknown>,
|
||||
piStatusesLoading: false,
|
||||
piStatusesError: false,
|
||||
}));
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
@@ -24,9 +30,14 @@ vi.mock("sonner", () => ({
|
||||
|
||||
vi.mock("@/hooks/useSkills", () => ({
|
||||
useInstalledSkills: () => ({
|
||||
data: [],
|
||||
data: skillsHookState.installed,
|
||||
isLoading: false,
|
||||
}),
|
||||
usePiSkillStatuses: () => ({
|
||||
data: skillsHookState.piStatuses,
|
||||
isLoading: skillsHookState.piStatusesLoading,
|
||||
isError: skillsHookState.piStatusesError,
|
||||
}),
|
||||
useSkillBackups: () => ({
|
||||
data: [],
|
||||
refetch: vi.fn(),
|
||||
@@ -94,6 +105,10 @@ describe("UnifiedSkillsPanel", () => {
|
||||
installFromZipMock.mockReset();
|
||||
deleteSkillBackupMock.mockReset();
|
||||
restoreSkillBackupMock.mockReset();
|
||||
skillsHookState.installed = [];
|
||||
skillsHookState.piStatuses = {};
|
||||
skillsHookState.piStatusesLoading = false;
|
||||
skillsHookState.piStatusesError = false;
|
||||
});
|
||||
|
||||
it("opens the import dialog without crashing when app toggles render", async () => {
|
||||
@@ -130,4 +145,61 @@ describe("UnifiedSkillsPanel", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders Pi active state from inspection and toggles the desired state", async () => {
|
||||
skillsHookState.installed = [
|
||||
{
|
||||
id: "skill-1",
|
||||
name: "Pi Skill",
|
||||
directory: "pi-skill",
|
||||
apps: {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
hermes: false,
|
||||
pi: true,
|
||||
},
|
||||
installedAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
];
|
||||
skillsHookState.piStatuses = {
|
||||
"skill-1": {
|
||||
desiredEnabled: true,
|
||||
ownedDeployment: false,
|
||||
effectivelyDiscovered: false,
|
||||
ownership: "foreign",
|
||||
discovery: "absent",
|
||||
issue: "collision",
|
||||
},
|
||||
};
|
||||
toggleSkillAppMock.mockResolvedValue(true);
|
||||
|
||||
render(
|
||||
<UnifiedSkillsPanel
|
||||
onOpenDiscovery={() => {}}
|
||||
currentApp="pi"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("Pi: skills.piStatus.foreignConflict"),
|
||||
).toBeInTheDocument();
|
||||
const piToggle = screen.getByRole("button", { name: "Pi" });
|
||||
expect(piToggle).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
await act(async () => {
|
||||
piToggle.click();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toggleSkillAppMock).toHaveBeenCalledWith({
|
||||
id: "skill-1",
|
||||
app: "pi",
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "@/i18n/locales/en.json";
|
||||
import ja from "@/i18n/locales/ja.json";
|
||||
import zhTW from "@/i18n/locales/zh-TW.json";
|
||||
import zh from "@/i18n/locales/zh.json";
|
||||
|
||||
type TranslationTree = Record<string, unknown>;
|
||||
|
||||
function flattenStrings(
|
||||
value: unknown,
|
||||
path: string[] = [],
|
||||
result = new Map<string, string>(),
|
||||
): Map<string, string> {
|
||||
if (typeof value === "string") {
|
||||
result.set(path.join("."), value);
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
flattenStrings(child, [...path, key], result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function interpolationVariables(value: string): string[] {
|
||||
return Array.from(
|
||||
value.matchAll(/\{\{\s*([^}]+?)\s*\}\}/g),
|
||||
([, name]) => name,
|
||||
).sort();
|
||||
}
|
||||
|
||||
const reference = flattenStrings(en);
|
||||
const piKeysOutsideNamespace = new Set([
|
||||
"apps.pi",
|
||||
"deeplink.api",
|
||||
"sessionManager.piDiscoveryUnavailable",
|
||||
"sessionManager.piRelativeSessionDir",
|
||||
"settings.browsePlaceholderPi",
|
||||
"settings.piConfigDir",
|
||||
"settings.piConfigDirDescription",
|
||||
"settings.piImportExportBoundary",
|
||||
"usage.appFilter.pi",
|
||||
]);
|
||||
const piReference = new Map(
|
||||
[...reference].filter(
|
||||
([key]) =>
|
||||
key.startsWith("pi.") ||
|
||||
key.startsWith("proxy.piGateway.") ||
|
||||
key.startsWith("skills.piStatus.") ||
|
||||
piKeysOutsideNamespace.has(key),
|
||||
),
|
||||
);
|
||||
const piProductReferences = new Map(
|
||||
[...reference].filter(([, value]) => /\bPi\b/.test(value)),
|
||||
);
|
||||
const locales = [
|
||||
["zh", zh],
|
||||
["ja", ja],
|
||||
["zh-TW", zhTW],
|
||||
] as const;
|
||||
|
||||
describe("locale coverage", () => {
|
||||
it.each(locales)("covers every Pi translation key in %s", (_name, tree) => {
|
||||
const translations = flattenStrings(tree as TranslationTree);
|
||||
const missing = [...piReference.keys()].filter(
|
||||
(key) => !translations.has(key),
|
||||
);
|
||||
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(locales)(
|
||||
"preserves every Pi interpolation variable in %s",
|
||||
(_name, tree) => {
|
||||
const translations = flattenStrings(tree as TranslationTree);
|
||||
const mismatched = [...piReference].flatMap(([key, expected]) => {
|
||||
const actual = translations.get(key);
|
||||
return actual !== undefined &&
|
||||
interpolationVariables(actual).join("\0") !==
|
||||
interpolationVariables(expected).join("\0")
|
||||
? [key]
|
||||
: [];
|
||||
});
|
||||
|
||||
expect(mismatched).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(locales)(
|
||||
"preserves explicit Pi product mentions in %s",
|
||||
(_name, tree) => {
|
||||
const translations = flattenStrings(tree as TranslationTree);
|
||||
const missingMentions = [...piProductReferences.keys()].filter((key) => {
|
||||
const actual = translations.get(key);
|
||||
return actual === undefined || !/\bPi\b/.test(actual);
|
||||
});
|
||||
|
||||
expect(missingMentions).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"codeAuthority": "src-tauri/src/architecture_tests.rs",
|
||||
"edges": [
|
||||
"composer->raw_schema",
|
||||
"gateway->composer",
|
||||
"native->composer",
|
||||
"native->document",
|
||||
"native->gateway",
|
||||
"native->model",
|
||||
"native->raw_schema"
|
||||
]
|
||||
}
|
||||
+2186
File diff suppressed because it is too large
Load Diff
+3000
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"version": 1,
|
||||
"pi": {
|
||||
"repository": "https://github.com/earendil-works/pi.git",
|
||||
"commit": "ab366ebe94cacd419d986be454f12b1b9913aaca"
|
||||
},
|
||||
"typeboxVersion": "1.3.7",
|
||||
"sources": {
|
||||
"modelConfig": {
|
||||
"path": "packages/coding-agent/src/core/model-config.ts",
|
||||
"sha256": "62141770d675ad6357a72e07354355f0eda29281c0e5be1b48d2360f341c7360"
|
||||
},
|
||||
"providerComposer": {
|
||||
"path": "packages/coding-agent/src/core/provider-composer.ts",
|
||||
"sha256": "17308a4179b330526eabf6c917fa13e9dbd9ece90d1555b870e87d39b5b60d9d"
|
||||
},
|
||||
"resolveConfigValue": {
|
||||
"path": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"sha256": "0f53dad47fe5d5d8837c022b7951ccd3bd5a9b577bd662f0986272110e83bcc7"
|
||||
}
|
||||
},
|
||||
"artifacts": {
|
||||
"provider-schema.snapshot.json": "e498c9f1b344eee1bd3c3ba74d1b648dcb835378cfad92800ec80078b825745c",
|
||||
"raw-oracle-v1.json": "5aaa37160f96a0fe50867d900ca38c73f13aba769e156a883324368d9dbeeb9a",
|
||||
"composer-oracle-v1.json": "f7e54bb84e5fd6d50e5762dc304834410fa73ef608c2f9c42475c5983f8e0cf5",
|
||||
"transport-oracle-v1.json": "b2c816e53b60da5cd6352d2c23934939e9f6dd0077971488fe9dd36fa723e855",
|
||||
"field-coverage-v1.json": "b8b85e611cf1dbef86c611df185ba8ac2d64160087d0c6e47747f838a0fafe42"
|
||||
},
|
||||
"harness": {
|
||||
"path": "scripts/generate-pi-native-oracle.mjs",
|
||||
"sha256": "f7a138831284b48ef655ef500a63313f0fc89cf08d319094895027ba4777cc20",
|
||||
"upstreamEntry": "packages/coding-agent/src/core/provider-composer.ts",
|
||||
"entryFunctions": [
|
||||
"composeModelProvider",
|
||||
"Provider.getModels",
|
||||
"resolveCompatibilityRequestConfig",
|
||||
"Provider.auth.apiKey.resolve"
|
||||
],
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"transportShims": {
|
||||
"pi-ai": "debdf80a40ad2086467c7c3ed6fe43d6167caa85e5bff86e10f28cc36d8fcc53",
|
||||
"pi-ai/compat": "533cd8f8a242bb25b42db8249abcb539daf30889ea748b5114c177e44c3849d2"
|
||||
},
|
||||
"assertion": "expected composer outputs were captured by executing the pinned upstream entry functions; transport shims are unreachable during credential-blind model composition",
|
||||
"transportResolver": {
|
||||
"upstreamEntry": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"entryFunctions": [
|
||||
"resolveConfigValueOrThrow",
|
||||
"resolveHeadersOrThrow"
|
||||
],
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"platform": "linux"
|
||||
}
|
||||
},
|
||||
"uncoveredSemantics": [
|
||||
{
|
||||
"id": "builtin-overlay-without-pinned-base-catalog",
|
||||
"unavailableContext": "pinned built-in Provider instance and model catalog",
|
||||
"rustExpectedStatus": "unknown",
|
||||
"reasonCode": "catalog_required"
|
||||
},
|
||||
{
|
||||
"id": "extension-overlay-without-extension-registration",
|
||||
"unavailableContext": "extension ProviderConfigInput and registered model catalog",
|
||||
"rustExpectedStatus": "unknown",
|
||||
"reasonCode": "catalog_required"
|
||||
},
|
||||
{
|
||||
"id": "radius-oauth-credential-lifecycle",
|
||||
"unavailableContext": "interactive Radius login, refresh credentials, and network exchange",
|
||||
"rustExpectedStatus": "direct_only",
|
||||
"reasonCode": "missing_gateway_credential"
|
||||
}
|
||||
],
|
||||
"schemaOperatorInventory": [
|
||||
"anyOf",
|
||||
"const",
|
||||
"items",
|
||||
"minLength",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
"required",
|
||||
"type"
|
||||
],
|
||||
"evaluatorOperatorAllowlist": [
|
||||
"additionalProperties",
|
||||
"anyOf",
|
||||
"const",
|
||||
"items",
|
||||
"minLength",
|
||||
"patternProperties",
|
||||
"properties",
|
||||
"required",
|
||||
"type"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+723
@@ -0,0 +1,723 @@
|
||||
{
|
||||
"version": 1,
|
||||
"execution": {
|
||||
"engine": "typebox Value.Check",
|
||||
"typeboxVersion": "1.3.7",
|
||||
"schemaTarget": "ModelsConfigSchema.properties.providers.patternProperties['^.*$']"
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"id": "all-schema-fields-valid",
|
||||
"input": {
|
||||
"name": "All Fields Provider",
|
||||
"baseUrl": "https://all-fields.example/v1",
|
||||
"apiKey": "literal-all-fields-key",
|
||||
"api": "openai-responses",
|
||||
"oauth": "radius",
|
||||
"headers": {
|
||||
"x-provider-field": "provider-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
},
|
||||
"authHeader": true,
|
||||
"models": [
|
||||
{
|
||||
"id": "all-fields-model",
|
||||
"name": "All Fields Model",
|
||||
"api": "anthropic-messages",
|
||||
"baseUrl": "https://all-fields-model.example/v1",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": "minimal-effort",
|
||||
"low": "low-effort",
|
||||
"medium": "medium-effort",
|
||||
"high": "high-effort",
|
||||
"xhigh": "xhigh-effort",
|
||||
"max": "max-effort"
|
||||
},
|
||||
"input": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0.11,
|
||||
"output": 0.22,
|
||||
"cacheRead": 0.033,
|
||||
"cacheWrite": 0.044,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.55,
|
||||
"output": 0.66,
|
||||
"cacheRead": 0.077,
|
||||
"cacheWrite": 0.088
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextWindow": 128000.5,
|
||||
"maxTokens": 16384.25,
|
||||
"headers": {
|
||||
"x-model-field": "model-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"modelOverrides": {
|
||||
"all-fields-model": {
|
||||
"name": "All Fields Override",
|
||||
"reasoning": false,
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": "minimal-effort",
|
||||
"low": "low-effort",
|
||||
"medium": "medium-effort",
|
||||
"high": "high-effort",
|
||||
"xhigh": "xhigh-effort",
|
||||
"max": "max-effort"
|
||||
},
|
||||
"input": [
|
||||
"image",
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0.11,
|
||||
"output": 0.22,
|
||||
"cacheRead": 0.033,
|
||||
"cacheWrite": 0.044,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.55,
|
||||
"output": 0.66,
|
||||
"cacheRead": 0.077,
|
||||
"cacheWrite": 0.088
|
||||
}
|
||||
]
|
||||
},
|
||||
"contextWindow": 256000.75,
|
||||
"maxTokens": 32768.5,
|
||||
"headers": {
|
||||
"x-override-field": "override-value"
|
||||
},
|
||||
"compat": {
|
||||
"supportsStore": true,
|
||||
"supportsDeveloperRole": true,
|
||||
"supportsReasoningEffort": true,
|
||||
"supportsUsageInStreaming": true,
|
||||
"maxTokensField": "max_completion_tokens",
|
||||
"requiresToolResultName": true,
|
||||
"requiresAssistantAfterToolResult": true,
|
||||
"requiresThinkingAsText": true,
|
||||
"requiresReasoningContentOnAssistantMessages": true,
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"stringValue": "literal",
|
||||
"numberValue": 1.25,
|
||||
"booleanValue": true,
|
||||
"nullValue": null,
|
||||
"variableValue": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
}
|
||||
},
|
||||
"cacheControlFormat": "anthropic",
|
||||
"openRouterRouting": {
|
||||
"allow_fallbacks": true,
|
||||
"require_parameters": true,
|
||||
"data_collection": "deny",
|
||||
"zdr": true,
|
||||
"enforce_distillable_text": true,
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
],
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"ignore": [
|
||||
"provider-z"
|
||||
],
|
||||
"quantizations": [
|
||||
"fp8"
|
||||
],
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.1,
|
||||
"completion": "2.2",
|
||||
"image": 3.3,
|
||||
"audio": "4.4",
|
||||
"request": 5.5
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p75": 9.5,
|
||||
"p90": 8.5,
|
||||
"p99": 7.5
|
||||
},
|
||||
"preferred_max_latency": {
|
||||
"p50": 100.5,
|
||||
"p75": 200.5,
|
||||
"p90": 300.5,
|
||||
"p99": 400.5
|
||||
}
|
||||
},
|
||||
"vercelGatewayRouting": {
|
||||
"only": [
|
||||
"provider-a"
|
||||
],
|
||||
"order": [
|
||||
"provider-a",
|
||||
"provider-b"
|
||||
]
|
||||
},
|
||||
"supportsOpenAIGrammarTools": true,
|
||||
"supportsStrictMode": true,
|
||||
"sendSessionAffinityHeaders": true,
|
||||
"deferredToolsMode": "kimi",
|
||||
"sessionAffinityFormat": "openrouter",
|
||||
"supportsLongCacheRetention": true,
|
||||
"supportsToolSearch": true,
|
||||
"supportsEagerToolInputStreaming": true,
|
||||
"supportsCacheControlOnTools": true,
|
||||
"supportsTemperature": true,
|
||||
"forceAdaptiveThinking": true,
|
||||
"allowEmptySignature": true,
|
||||
"supportsStrictTools": true,
|
||||
"supportsToolReferences": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "empty-provider-object",
|
||||
"input": {},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "null-provider",
|
||||
"input": null,
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "additional-provider-property",
|
||||
"input": {
|
||||
"futureProviderField": {
|
||||
"nested": true
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "empty-present-base-url",
|
||||
"input": {
|
||||
"baseUrl": ""
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "non-url-base-url-is-raw-string",
|
||||
"input": {
|
||||
"baseUrl": "not a URL"
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "radius-oauth",
|
||||
"input": {
|
||||
"oauth": "radius"
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "unknown-oauth-literal",
|
||||
"input": {
|
||||
"oauth": "other"
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "models-null",
|
||||
"input": {
|
||||
"models": null
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-missing-id",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"api": "openai-responses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-empty-id",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "integer-model-numbers",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "integer",
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 16384
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "fractional-model-numbers",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "fractional",
|
||||
"contextWindow": 128000.5,
|
||||
"maxTokens": 16384.25
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "negative-number-is-still-typebox-number",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "negative",
|
||||
"maxTokens": -1.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "string-is-not-number",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "string-limit",
|
||||
"maxTokens": "16384"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "complete-cost-with-fractions",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "priced",
|
||||
"cost": {
|
||||
"input": 0.1,
|
||||
"output": 0.2,
|
||||
"cacheRead": 0.03,
|
||||
"cacheWrite": 0.04,
|
||||
"tiers": [
|
||||
{
|
||||
"inputTokensAbove": 1000.5,
|
||||
"input": 0.5,
|
||||
"output": 0.6,
|
||||
"cacheRead": 0.07,
|
||||
"cacheWrite": 0.08
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "incomplete-model-cost",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "priced",
|
||||
"cost": {
|
||||
"input": 0.1,
|
||||
"output": 0.2,
|
||||
"cacheRead": 0.03
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-openrouter-recursive-valid",
|
||||
"input": {
|
||||
"compat": {
|
||||
"thinkingFormat": "chat-template",
|
||||
"chatTemplateKwargs": {
|
||||
"temperature": 0.25,
|
||||
"thinking": {
|
||||
"$var": "thinking.effort",
|
||||
"omitWhenOff": true
|
||||
},
|
||||
"nullable": null
|
||||
},
|
||||
"openRouterRouting": {
|
||||
"data_collection": "deny",
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
},
|
||||
"max_price": {
|
||||
"prompt": 1.5,
|
||||
"completion": "2.0"
|
||||
},
|
||||
"preferred_min_throughput": {
|
||||
"p50": 10.5,
|
||||
"p99": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-nested-union-invalid-in-every-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"preferred_min_throughput": {
|
||||
"p50": "fast"
|
||||
}
|
||||
},
|
||||
"supportsToolSearch": "yes",
|
||||
"supportsTemperature": 1
|
||||
}
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-union-additional-field",
|
||||
"input": {
|
||||
"compat": {
|
||||
"futureCompatField": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-null",
|
||||
"input": {
|
||||
"compat": null
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "thinking-map-unknown-key-and-string-value",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "thinking",
|
||||
"thinkingLevelMap": {
|
||||
"low": null,
|
||||
"max": "future-effort",
|
||||
"future": {
|
||||
"nested": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "thinking-map-known-key-invalid-value",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "thinking",
|
||||
"thinkingLevelMap": {
|
||||
"low": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-string-union-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": "price"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-object-union-branch",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": {
|
||||
"by": "price",
|
||||
"partition": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "compat-sort-invalid-union-boundary",
|
||||
"input": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"sort": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "headers-record-valid",
|
||||
"input": {
|
||||
"headers": {
|
||||
"Authorization": "Bearer literal",
|
||||
"x-count": "2"
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "headers-record-invalid-value",
|
||||
"input": {
|
||||
"headers": {
|
||||
"x-count": 2
|
||||
}
|
||||
},
|
||||
"expectedValid": false
|
||||
},
|
||||
{
|
||||
"id": "model-override-fractional-and-extra",
|
||||
"input": {
|
||||
"modelOverrides": {
|
||||
"model/a": {
|
||||
"contextWindow": 10.5,
|
||||
"maxTokens": 2.25,
|
||||
"futureOverrideField": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedValid": true
|
||||
},
|
||||
{
|
||||
"id": "input-union-invalid",
|
||||
"input": {
|
||||
"models": [
|
||||
{
|
||||
"id": "input",
|
||||
"input": [
|
||||
"text",
|
||||
"audio"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"expectedValid": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"version": 1,
|
||||
"piCommit": "ab366ebe94cacd419d986be454f12b1b9913aaca",
|
||||
"engine": "pinned upstream TypeScript",
|
||||
"bundler": "esbuild@0.28.1",
|
||||
"upstreamEntry": "packages/coding-agent/src/core/resolve-config-value.ts",
|
||||
"platform": "linux",
|
||||
"cases": [
|
||||
{
|
||||
"id": "literal-value",
|
||||
"input": "literal-secret",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "literal-secret"
|
||||
},
|
||||
{
|
||||
"id": "environment-template",
|
||||
"input": "prefix-${PI_ORACLE_VALUE}-suffix",
|
||||
"environment": {
|
||||
"PI_ORACLE_VALUE": "environment-secret"
|
||||
},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "prefix-environment-secret-suffix"
|
||||
},
|
||||
{
|
||||
"id": "escaped-dollar-and-bang",
|
||||
"input": "$$literal-$!bang",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "$literal-!bang"
|
||||
},
|
||||
{
|
||||
"id": "shell-command",
|
||||
"input": "!printf pi-command-value",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expected": "pi-command-value"
|
||||
},
|
||||
{
|
||||
"id": "missing-environment",
|
||||
"input": "${PI_ORACLE_MISSING}",
|
||||
"environment": {},
|
||||
"execution": {
|
||||
"status": "error",
|
||||
"entryFunction": "resolveConfigValueOrThrow"
|
||||
},
|
||||
"expectedError": "Failed to resolve oracle missing-environment from environment variable: PI_ORACLE_MISSING"
|
||||
}
|
||||
],
|
||||
"headerCase": {
|
||||
"id": "provider-header-materialization",
|
||||
"input": {
|
||||
"x-literal": "literal-header",
|
||||
"x-environment": "${PI_ORACLE_HEADER}",
|
||||
"x-command": "!printf pi-command-header"
|
||||
},
|
||||
"environment": {
|
||||
"PI_ORACLE_HEADER": "environment-header"
|
||||
},
|
||||
"execution": {
|
||||
"status": "success",
|
||||
"entryFunction": "resolveHeadersOrThrow"
|
||||
},
|
||||
"expected": {
|
||||
"x-literal": "literal-header",
|
||||
"x-environment": "environment-header",
|
||||
"x-command": "pi-command-header"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ describe("useDirectorySettings", () => {
|
||||
if (app === "grokbuild") return "/remote/grok";
|
||||
if (app === "opencode") return "/remote/opencode";
|
||||
if (app === "openclaw") return "/remote/openclaw";
|
||||
if (app === "pi") return "/remote/pi";
|
||||
return "/remote/hermes";
|
||||
});
|
||||
selectConfigDirectoryMock.mockReset();
|
||||
@@ -96,6 +97,7 @@ describe("useDirectorySettings", () => {
|
||||
opencode: "/remote/opencode",
|
||||
openclaw: "/remote/openclaw",
|
||||
hermes: "/remote/hermes",
|
||||
pi: "/remote/pi",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ function makeSkill(overrides: Partial<InstalledSkill> = {}): InstalledSkill {
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
hermes: false,
|
||||
pi: false,
|
||||
},
|
||||
installedAt: 0,
|
||||
updatedAt: 0,
|
||||
|
||||
@@ -131,4 +131,74 @@ describe("useProxyStatus", () => {
|
||||
expect(invokeMock).toHaveBeenCalledWith("start_proxy_server");
|
||||
expect(invokeMock).toHaveBeenCalledWith("stop_proxy_server");
|
||||
});
|
||||
|
||||
it("refreshes desired takeover state even when activation rejects", async () => {
|
||||
let desiredPi = false;
|
||||
invokeMock.mockImplementation((command: string) => {
|
||||
if (command === "get_proxy_status") {
|
||||
return Promise.resolve({
|
||||
running: false,
|
||||
address: "127.0.0.1",
|
||||
port: 15721,
|
||||
active_connections: 0,
|
||||
total_requests: 0,
|
||||
success_requests: 0,
|
||||
failed_requests: 0,
|
||||
success_rate: 0,
|
||||
uptime_seconds: 0,
|
||||
current_provider: null,
|
||||
current_provider_id: null,
|
||||
last_request_at: null,
|
||||
last_error: null,
|
||||
failover_count: 0,
|
||||
});
|
||||
}
|
||||
if (command === "get_proxy_takeover_status") {
|
||||
return Promise.resolve({
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
grokbuild: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
pi: desiredPi,
|
||||
piOperationalState: desiredPi ? "degraded" : "disabled",
|
||||
});
|
||||
}
|
||||
if (command === "set_proxy_takeover_for_app") {
|
||||
desiredPi = true;
|
||||
return Promise.reject(new Error("listener unavailable"));
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
const { wrapper, queryClient } = createWrapper();
|
||||
const { result, rerender } = renderHook(() => useProxyStatus(), {
|
||||
wrapper,
|
||||
});
|
||||
await waitFor(() => expect(result.current.takeoverStatus).toBeDefined());
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.setTakeoverForApp({
|
||||
appType: "pi",
|
||||
enabled: true,
|
||||
});
|
||||
}),
|
||||
).rejects.toThrow("listener unavailable");
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
invokeMock.mock.calls.filter(
|
||||
([command]) => command === "get_proxy_takeover_status",
|
||||
).length,
|
||||
).toBeGreaterThan(1),
|
||||
);
|
||||
expect(queryClient.getQueryData(proxyKeys.takeoverStatus)).toMatchObject({
|
||||
pi: true,
|
||||
piOperationalState: "degraded",
|
||||
});
|
||||
rerender();
|
||||
await waitFor(() => expect(result.current.takeoverStatus?.pi).toBe(true));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,6 +92,8 @@ const createSettingsFormMock = (overrides: Record<string, unknown> = {}) => ({
|
||||
geminiConfigDir: "/gemini",
|
||||
opencodeConfigDir: "/opencode",
|
||||
openclawConfigDir: "/openclaw",
|
||||
hermesConfigDir: "/hermes",
|
||||
piConfigDir: "/pi",
|
||||
language: "zh",
|
||||
},
|
||||
isLoading: false,
|
||||
@@ -113,6 +115,8 @@ const createDirectorySettingsMock = (
|
||||
gemini: "/default/gemini",
|
||||
opencode: "/default/opencode",
|
||||
openclaw: "/default/openclaw",
|
||||
hermes: "/default/hermes",
|
||||
pi: "/default/pi",
|
||||
},
|
||||
isLoading: false,
|
||||
initialAppConfigDir: undefined,
|
||||
@@ -161,6 +165,8 @@ describe("useSettings hook", () => {
|
||||
geminiConfigDir: "/server/gemini",
|
||||
opencodeConfigDir: "/server/opencode",
|
||||
openclawConfigDir: "/server/openclaw",
|
||||
hermesConfigDir: "/server/hermes",
|
||||
piConfigDir: "/server/pi",
|
||||
language: "zh",
|
||||
};
|
||||
|
||||
@@ -348,6 +354,25 @@ describe("useSettings hook", () => {
|
||||
expect(syncCurrentProvidersLiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sanitizes and republishes when the Pi native directory changes", async () => {
|
||||
settingsFormMock = createSettingsFormMock({
|
||||
settings: {
|
||||
...serverSettings,
|
||||
piConfigDir: " /custom/pi ",
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useSettings());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.saveSettings(undefined, { silent: true });
|
||||
});
|
||||
|
||||
const payload = mutateAsyncMock.mock.calls[0][0] as Settings;
|
||||
expect(payload.piConfigDir).toBe("/custom/pi");
|
||||
expect(syncCurrentProvidersLiveMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shows toast when Claude plugin sync fails but continues flow", async () => {
|
||||
// 设置服务器状态为 false,本地状态为 true,触发状态变化
|
||||
serverSettings = {
|
||||
@@ -459,9 +484,11 @@ describe("useSettings hook", () => {
|
||||
claude: "/server/claude",
|
||||
codex: undefined,
|
||||
gemini: "/server/gemini",
|
||||
grokbuild: undefined,
|
||||
opencode: "/server/opencode",
|
||||
openclaw: "/server/openclaw",
|
||||
hermes: undefined,
|
||||
hermes: "/server/hermes",
|
||||
pi: "/server/pi",
|
||||
});
|
||||
expect(metadataMock.setRequiresRestart).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
@@ -170,6 +170,8 @@ describe("useSettingsForm Hook", () => {
|
||||
enableClaudePluginIntegration: true,
|
||||
claudeConfigDir: " /reset ",
|
||||
codexConfigDir: " ",
|
||||
hermesConfigDir: " /hermes-reset ",
|
||||
piConfigDir: " /pi-reset ",
|
||||
language: "zh",
|
||||
});
|
||||
});
|
||||
@@ -180,6 +182,8 @@ describe("useSettingsForm Hook", () => {
|
||||
expect(settings.enableClaudePluginIntegration).toBe(true);
|
||||
expect(settings.claudeConfigDir).toBe("/reset");
|
||||
expect(settings.codexConfigDir).toBeUndefined();
|
||||
expect(settings.hermesConfigDir).toBe("/hermes-reset");
|
||||
expect(settings.piConfigDir).toBe("/pi-reset");
|
||||
expect(settings.language).toBe("zh");
|
||||
expect(result.current.initialLanguage).toBe("en");
|
||||
expect(changeLanguageSpy).toHaveBeenCalledWith("en");
|
||||
|
||||
@@ -73,6 +73,7 @@ const createDefaultProviders = (): ProvidersByApp => ({
|
||||
opencode: {},
|
||||
openclaw: {},
|
||||
hermes: {},
|
||||
pi: {},
|
||||
});
|
||||
|
||||
const createDefaultCurrent = (): CurrentProviderState => ({
|
||||
@@ -84,6 +85,7 @@ const createDefaultCurrent = (): CurrentProviderState => ({
|
||||
opencode: "",
|
||||
openclaw: "",
|
||||
hermes: "",
|
||||
pi: "",
|
||||
});
|
||||
|
||||
let providers = createDefaultProviders();
|
||||
@@ -197,6 +199,7 @@ let mcpConfigs: McpConfigState = {
|
||||
opencode: {},
|
||||
openclaw: {},
|
||||
hermes: {},
|
||||
pi: {},
|
||||
};
|
||||
|
||||
const cloneProviders = (value: ProvidersByApp) =>
|
||||
@@ -266,6 +269,7 @@ export const resetProviderState = () => {
|
||||
opencode: {},
|
||||
openclaw: {},
|
||||
hermes: {},
|
||||
pi: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getCacheWriteAvailability } from "@/types/usage";
|
||||
|
||||
describe("getCacheWriteAvailability", () => {
|
||||
it("does not present Pi mixed-protocol cache creation as an authoritative zero", () => {
|
||||
expect(getCacheWriteAvailability(["pi"])).toBe("partial");
|
||||
expect(getCacheWriteAvailability(["pi", "codex"])).toBe("partial");
|
||||
expect(getCacheWriteAvailability(["pi", "claude"])).toBe("partial");
|
||||
});
|
||||
|
||||
it("preserves fixed-protocol and cross-app availability states", () => {
|
||||
expect(getCacheWriteAvailability(["claude"])).toBe("ok");
|
||||
expect(getCacheWriteAvailability(["codex", "gemini"])).toBe("na");
|
||||
expect(getCacheWriteAvailability(["claude", "codex"])).toBe("partial");
|
||||
expect(getCacheWriteAvailability([])).toBe("ok");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user