mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Make app-server initialize profile explicit
This commit is contained in:
parent
a9e68e159d
commit
42af40c299
13 changed files with 301 additions and 170 deletions
|
|
@ -82,4 +82,4 @@ npm run api:baseline
|
|||
|
||||
Obsidian runtime compatibility is declared through `manifest.json` and `versions.json`. The `obsidian` npm package provides compile-time TypeScript API definitions; it is not runtime validation for an Obsidian app-version matrix. Because the project does not run app-version smoke tests, keep the API type package in the same minor as `manifest.minAppVersion` and use the latest patch in that minor for local type checking. `npm run api:baseline` exits non-zero when the local environment or recorded baselines drift. Raise `manifest.minAppVersion` only when intentionally adopting a newer Obsidian app/API minor.
|
||||
|
||||
Codex app-server compatibility is managed by Codex CLI minor version. README records the tested Codex CLI patch version, and the baseline check verifies that the local `codex --version` is in the same minor before app-server binding or compatibility work.
|
||||
Codex app-server compatibility is managed by Codex CLI minor version. README records the tested Codex CLI patch version, `src/app-server/connection/compatibility.json` records the machine-readable app-server capability baseline used by the panel client profile, and the baseline check verifies that the local `codex --version` is in the same minor before app-server binding or compatibility work.
|
||||
|
|
|
|||
|
|
@ -73,12 +73,14 @@ export async function createApiBaselineReport(options = {}) {
|
|||
const obsidianLockSemver = parseSemver(obsidianLockVersion);
|
||||
const obsidianMinSemver = parseSemver(obsidianMinVersion);
|
||||
|
||||
const appServerGenerationExperimentalDeclared = inputs.appServerCompatibilityJson.codexAppServer?.typeGeneration?.experimental === true;
|
||||
const appServerGenerationExperimental =
|
||||
inputs.appServerGenerateSource.includes("app-server") &&
|
||||
inputs.appServerGenerateSource.includes("generate-ts") &&
|
||||
inputs.appServerGenerateSource.includes("--experimental");
|
||||
const initializeExperimentalApi = /experimentalApi:\s*true/.test(inputs.clientSource);
|
||||
const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(inputs.clientSource);
|
||||
const initializeCapabilities = inputs.appServerCompatibilityJson.codexAppServer?.initialize?.capabilities ?? {};
|
||||
const initializeExperimentalApi = initializeCapabilities.experimentalApi === true;
|
||||
const initializeRequestAttestationDisabled = initializeCapabilities.requestAttestation === false;
|
||||
|
||||
if (!codexReadmeSemver) {
|
||||
fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z.");
|
||||
|
|
@ -89,9 +91,19 @@ export async function createApiBaselineReport(options = {}) {
|
|||
if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) {
|
||||
fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`);
|
||||
}
|
||||
if (!appServerGenerationExperimentalDeclared) {
|
||||
fail("src/app-server/connection/compatibility.json must declare codexAppServer.typeGeneration.experimental: true.");
|
||||
}
|
||||
if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental.");
|
||||
if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true.");
|
||||
if (!initializeRequestAttestationDisabled) fail("app-server initialize must declare requestAttestation: false.");
|
||||
if (appServerGenerationExperimentalDeclared !== appServerGenerationExperimental) {
|
||||
fail("app-server type generation declaration must match scripts/generate-app-server-types.mjs.");
|
||||
}
|
||||
if (!initializeExperimentalApi) {
|
||||
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.experimentalApi: true.");
|
||||
}
|
||||
if (!initializeRequestAttestationDisabled) {
|
||||
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.requestAttestation: false.");
|
||||
}
|
||||
|
||||
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
|
||||
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
|
||||
|
|
@ -132,6 +144,7 @@ export async function createApiBaselineReport(options = {}) {
|
|||
localCliVersion: codexLocalVersion,
|
||||
localCliMinor: minorKey(codexLocalSemver),
|
||||
localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null,
|
||||
appServerGenerationExperimentalDeclared,
|
||||
appServerGenerationExperimental,
|
||||
initializeExperimentalApi,
|
||||
initializeRequestAttestationDisabled,
|
||||
|
|
@ -225,19 +238,20 @@ function displayValue(value) {
|
|||
}
|
||||
|
||||
async function readBaselineInputs(cwd) {
|
||||
const [packageJson, packageLockJson, manifestJson, versionsJson, readme, clientSource, appServerGenerateSource] = await Promise.all([
|
||||
readJson(cwd, "package.json"),
|
||||
readJson(cwd, "package-lock.json"),
|
||||
readJson(cwd, "manifest.json"),
|
||||
readJson(cwd, "versions.json"),
|
||||
readFile(path.join(cwd, "README.md"), "utf8"),
|
||||
readFile(path.join(cwd, "src/app-server/connection/client.ts"), "utf8"),
|
||||
readFile(path.join(cwd, "scripts/generate-app-server-types.mjs"), "utf8"),
|
||||
]);
|
||||
const [packageJson, packageLockJson, manifestJson, versionsJson, readme, appServerCompatibilityJson, appServerGenerateSource] =
|
||||
await Promise.all([
|
||||
readJson(cwd, "package.json"),
|
||||
readJson(cwd, "package-lock.json"),
|
||||
readJson(cwd, "manifest.json"),
|
||||
readJson(cwd, "versions.json"),
|
||||
readFile(path.join(cwd, "README.md"), "utf8"),
|
||||
readJson(cwd, "src/app-server/connection/compatibility.json"),
|
||||
readFile(path.join(cwd, "scripts/generate-app-server-types.mjs"), "utf8"),
|
||||
]);
|
||||
|
||||
return {
|
||||
appServerCompatibilityJson,
|
||||
appServerGenerateSource,
|
||||
clientSource,
|
||||
manifestJson,
|
||||
packageJson,
|
||||
packageLockJson,
|
||||
|
|
@ -263,6 +277,7 @@ function printReport(report) {
|
|||
console.log(` compatibility table minor: ${displayValue(report.codex.readmeTestedMinor)}`);
|
||||
console.log(` local codex CLI: ${displayValue(report.codex.localCliVersion)}`);
|
||||
console.log(` local codex minor: ${displayValue(report.codex.localCliMinor)}`);
|
||||
console.log(` declared generate-ts --experimental: ${report.codex.appServerGenerationExperimentalDeclared ? "yes" : "no"}`);
|
||||
console.log(` generate-ts --experimental: ${report.codex.appServerGenerationExperimental ? "yes" : "no"}`);
|
||||
console.log(` initialize experimentalApi: ${report.codex.initializeExperimentalApi ? "yes" : "no"}`);
|
||||
console.log(` initialize requestAttestation disabled: ${report.codex.initializeRequestAttestationDisabled ? "yes" : "no"}`);
|
||||
|
|
|
|||
31
src/app-server/connection/client-profile.ts
Normal file
31
src/app-server/connection/client-profile.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { CLIENT_VERSION } from "../../constants";
|
||||
import type { InitializeCapabilities } from "../../generated/app-server/InitializeCapabilities";
|
||||
import type { InitializeParams } from "../../generated/app-server/InitializeParams";
|
||||
import compatibility from "./compatibility.json";
|
||||
|
||||
interface AppServerCompatibility {
|
||||
codexAppServer: {
|
||||
typeGeneration: {
|
||||
experimental: boolean;
|
||||
};
|
||||
initialize: {
|
||||
capabilities: Pick<InitializeCapabilities, "experimentalApi" | "requestAttestation">;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const appServerCompatibility = compatibility satisfies AppServerCompatibility;
|
||||
|
||||
export function codexPanelAppServerInitializeParams(): InitializeParams {
|
||||
return {
|
||||
clientInfo: {
|
||||
name: "obsidian_codex_panel",
|
||||
title: "Codex Panel",
|
||||
version: CLIENT_VERSION,
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: appServerCompatibility.codexAppServer.initialize.capabilities.experimentalApi,
|
||||
requestAttestation: appServerCompatibility.codexAppServer.initialize.capabilities.requestAttestation,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { CLIENT_VERSION } from "../../constants";
|
||||
import type { InitializeParams } from "../../generated/app-server/InitializeParams";
|
||||
import type { InitializeResponse } from "../../generated/app-server/InitializeResponse";
|
||||
import type { RequestId } from "../../generated/app-server/RequestId";
|
||||
import type { ServerNotification } from "../../generated/app-server/ServerNotification";
|
||||
|
|
@ -57,6 +57,15 @@ export interface AppServerServerRequestResponder {
|
|||
|
||||
export type AppServerTransportFactory = (handlers: AppServerTransportHandlers) => AppServerTransport;
|
||||
|
||||
export interface AppServerClientOptions {
|
||||
codexPath: string;
|
||||
cwd: string;
|
||||
handlers: AppServerClientHandlers;
|
||||
initializeParams: InitializeParams;
|
||||
requestTimeoutMs?: number;
|
||||
transportFactory?: AppServerTransportFactory;
|
||||
}
|
||||
|
||||
export interface ClientResponseByMethod {
|
||||
initialize: InitializeResponse;
|
||||
"config/batchWrite": ConfigWriteResponse;
|
||||
|
|
@ -106,14 +115,20 @@ export class AppServerClient {
|
|||
private lifecycle: AppServerClientLifecycleState = { kind: "disconnected" };
|
||||
private readonly rpc: JsonRpcClient;
|
||||
private readonly intentionallyStoppedTransports = new WeakSet<AppServerTransport>();
|
||||
private readonly codexPath: string;
|
||||
private readonly cwd: string;
|
||||
private readonly handlers: AppServerClientHandlers;
|
||||
private readonly initializeParams: InitializeParams;
|
||||
private readonly requestTimeoutMs: number;
|
||||
private readonly transportFactory: AppServerTransportFactory | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly codexPath: string,
|
||||
private readonly cwd: string,
|
||||
private readonly handlers: AppServerClientHandlers,
|
||||
private readonly requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
private readonly transportFactory?: AppServerTransportFactory,
|
||||
) {
|
||||
constructor(options: AppServerClientOptions) {
|
||||
this.codexPath = options.codexPath;
|
||||
this.cwd = options.cwd;
|
||||
this.handlers = options.handlers;
|
||||
this.initializeParams = options.initializeParams;
|
||||
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
||||
this.transportFactory = options.transportFactory;
|
||||
this.rpc = new JsonRpcClient({
|
||||
requestTimeoutMs: this.requestTimeoutMs,
|
||||
send: (message) => {
|
||||
|
|
@ -178,17 +193,7 @@ export class AppServerClient {
|
|||
this.lifecycle = { kind: "starting", transport };
|
||||
try {
|
||||
transport.start();
|
||||
const init = await this.request("initialize", {
|
||||
clientInfo: {
|
||||
name: "obsidian_codex_panel",
|
||||
title: "Codex Panel",
|
||||
version: CLIENT_VERSION,
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
requestAttestation: false,
|
||||
},
|
||||
});
|
||||
const init = await this.request("initialize", this.initializeParams);
|
||||
this.notify({ method: "initialized" });
|
||||
this.lifecycle = { kind: "initialized", transport, initializeResponse: init };
|
||||
return init;
|
||||
|
|
|
|||
13
src/app-server/connection/compatibility.json
Normal file
13
src/app-server/connection/compatibility.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"codexAppServer": {
|
||||
"typeGeneration": {
|
||||
"experimental": true
|
||||
},
|
||||
"initialize": {
|
||||
"capabilities": {
|
||||
"experimentalApi": true,
|
||||
"requestAttestation": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ServerInitialization } from "../../domain/server/initialization";
|
||||
import type { InitializeParams } from "../../generated/app-server/InitializeParams";
|
||||
import type { ServerNotification } from "../../generated/app-server/ServerNotification";
|
||||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
import { AppServerClient, type AppServerClientHandlers } from "./client";
|
||||
|
|
@ -43,7 +44,9 @@ export class ConnectionManager {
|
|||
constructor(
|
||||
private readonly codexPath: () => string,
|
||||
private readonly cwd: string,
|
||||
private readonly clientFactory: AppServerClientFactory = (codexPath, cwd, handlers) => new AppServerClient(codexPath, cwd, handlers),
|
||||
initializeParams: InitializeParams,
|
||||
private readonly clientFactory: AppServerClientFactory = (codexPath, cwd, handlers) =>
|
||||
new AppServerClient({ codexPath, cwd, handlers, initializeParams }),
|
||||
) {}
|
||||
|
||||
currentClient(): AppServerClient | null {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { AppServerClient } from "./client";
|
||||
import type { AppServerClientAccessOptions } from "./client-access";
|
||||
import { codexPanelAppServerInitializeParams } from "./client-profile";
|
||||
|
||||
export async function withShortLivedAppServerClient<T>(
|
||||
codexPath: string,
|
||||
|
|
@ -7,19 +8,24 @@ export async function withShortLivedAppServerClient<T>(
|
|||
operation: (client: AppServerClient) => Promise<T>,
|
||||
options: AppServerClientAccessOptions = {},
|
||||
): Promise<T> {
|
||||
const client = new AppServerClient(codexPath, cwd, {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: (request, responder) => {
|
||||
void request;
|
||||
responder.reject(
|
||||
-32601,
|
||||
options.serverRequests?.kind === "reject"
|
||||
? options.serverRequests.message
|
||||
: "This Codex Panel view does not handle server requests.",
|
||||
);
|
||||
const client = new AppServerClient({
|
||||
codexPath,
|
||||
cwd,
|
||||
initializeParams: codexPanelAppServerInitializeParams(),
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: (request, responder) => {
|
||||
void request;
|
||||
responder.reject(
|
||||
-32601,
|
||||
options.serverRequests?.kind === "reject"
|
||||
? options.serverRequests.message
|
||||
: "This Codex Panel view does not handle server requests.",
|
||||
);
|
||||
},
|
||||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
},
|
||||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { listenAbortSignal } from "../../shared/runtime/abort-signal";
|
||||
import { AppServerClient, type AppServerClientHandlers } from "../connection/client";
|
||||
import type { AppServerClientRequestPolicy } from "../connection/client-access";
|
||||
import { codexPanelAppServerInitializeParams } from "../connection/client-profile";
|
||||
import type { ServerNotification } from "../connection/rpc-messages";
|
||||
import { lastAgentMessageTextFromTurnRecord, type TurnItem, type TurnRecord } from "../protocol/turn";
|
||||
import type { ModelMetadataClient } from "./catalog";
|
||||
|
|
@ -96,7 +97,15 @@ export async function runEphemeralStructuredTurn(options: RunEphemeralStructured
|
|||
};
|
||||
});
|
||||
|
||||
const clientFactory = options.clientFactory ?? ((codexPath, cwd, handlers) => new AppServerClient(codexPath, cwd, handlers));
|
||||
const clientFactory =
|
||||
options.clientFactory ??
|
||||
((codexPath, cwd, handlers) =>
|
||||
new AppServerClient({
|
||||
codexPath,
|
||||
cwd,
|
||||
handlers,
|
||||
initializeParams: codexPanelAppServerInitializeParams(),
|
||||
}));
|
||||
let threadId: string | null = null;
|
||||
const client = clientFactory(options.codexPath, options.cwd, {
|
||||
onNotification: (notification) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { codexPanelAppServerInitializeParams } from "../../../app-server/connection/client-profile";
|
||||
import { ConnectionManager } from "../../../app-server/connection/connection-manager";
|
||||
import { isStaleAppServerSharedQueryContextError } from "../../../app-server/query/shared-queries";
|
||||
import { createChatAppServerGateway } from "../app-server/session-gateway";
|
||||
|
|
@ -272,7 +273,11 @@ export function createChatPanelSessionGraph(host: ChatPanelSessionGraphHost): Ch
|
|||
}
|
||||
|
||||
function createConnectionManager(environment: ChatPanelEnvironment): ConnectionManager {
|
||||
return new ConnectionManager(() => environment.plugin.settingsRef.settings.codexPath(), environment.plugin.settingsRef.vaultPath);
|
||||
return new ConnectionManager(
|
||||
() => environment.plugin.settingsRef.settings.codexPath(),
|
||||
environment.plugin.settingsRef.vaultPath,
|
||||
codexPanelAppServerInitializeParams(),
|
||||
);
|
||||
}
|
||||
|
||||
function createSessionStatus(stateStore: ChatStateStore, localItemIds: LocalIdSource): ChatPanelSessionStatus {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import manifest from "../../manifest.json";
|
||||
import type { AppServerServerRequestResponder } from "../../src/app-server/connection/client";
|
||||
import type { AppServerClientHandlers, AppServerServerRequestResponder } from "../../src/app-server/connection/client";
|
||||
import { AppServerClient } from "../../src/app-server/connection/client";
|
||||
import type { RpcOutboundMessage } from "../../src/app-server/connection/rpc-messages";
|
||||
import type { AppServerTransport, AppServerTransportHandlers } from "../../src/app-server/connection/transport";
|
||||
import type { InitializeParams } from "../../src/generated/app-server/InitializeParams";
|
||||
import type { InitializeResponse } from "../../src/generated/app-server/InitializeResponse";
|
||||
import type { ServerRequest } from "../../src/generated/app-server/ServerRequest";
|
||||
|
||||
const TEST_INITIALIZE_PARAMS: InitializeParams = {
|
||||
clientInfo: {
|
||||
name: "test_client",
|
||||
title: "Test Client",
|
||||
version: "0.0.0",
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: false,
|
||||
requestAttestation: false,
|
||||
},
|
||||
};
|
||||
|
||||
class FakeTransport implements AppServerTransport {
|
||||
readonly sent: RpcOutboundMessage[] = [];
|
||||
running = false;
|
||||
|
|
@ -45,27 +57,40 @@ class FakeTransport implements AppServerTransport {
|
|||
|
||||
async function connectedClient(): Promise<{ client: AppServerClient; transport: FakeTransport }> {
|
||||
let transport!: FakeTransport;
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
return { client, transport };
|
||||
}
|
||||
|
||||
function createTestClient(options: {
|
||||
handlers: AppServerClientHandlers;
|
||||
requestTimeoutMs?: number;
|
||||
transportFactory: (handlers: AppServerTransportHandlers) => AppServerTransport;
|
||||
initializeParams?: InitializeParams;
|
||||
}): AppServerClient {
|
||||
return new AppServerClient({
|
||||
codexPath: "/bin/codex",
|
||||
cwd: "/vault",
|
||||
handlers: options.handlers,
|
||||
initializeParams: options.initializeParams ?? TEST_INITIALIZE_PARAMS,
|
||||
requestTimeoutMs: options.requestTimeoutMs ?? 500,
|
||||
transportFactory: options.transportFactory,
|
||||
});
|
||||
}
|
||||
|
||||
function expectTransport(transport: FakeTransport | undefined): FakeTransport {
|
||||
if (!transport) throw new Error("Expected app-server transport.");
|
||||
return transport;
|
||||
|
|
@ -117,10 +142,19 @@ describe("AppServerClient", () => {
|
|||
const notifications: string[] = [];
|
||||
const serverRequests: ServerRequest[] = [];
|
||||
const serverRequestResponders: AppServerServerRequestResponder[] = [];
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
initializeParams: {
|
||||
clientInfo: {
|
||||
name: "custom_client",
|
||||
title: "Custom Client",
|
||||
version: "1.2.3",
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
requestAttestation: false,
|
||||
},
|
||||
},
|
||||
handlers: {
|
||||
onNotification: (notification) => notifications.push(notification.method),
|
||||
onServerRequest: (request, responder) => {
|
||||
serverRequests.push(request);
|
||||
|
|
@ -129,12 +163,11 @@ describe("AppServerClient", () => {
|
|||
onLog: () => undefined,
|
||||
onExit: () => undefined,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const connecting = client.connect();
|
||||
expect(getTransport().sent[0]).toMatchObject({
|
||||
|
|
@ -142,9 +175,9 @@ describe("AppServerClient", () => {
|
|||
method: "initialize",
|
||||
params: {
|
||||
clientInfo: {
|
||||
name: "obsidian_codex_panel",
|
||||
title: "Codex Panel",
|
||||
version: manifest.version,
|
||||
name: "custom_client",
|
||||
title: "Custom Client",
|
||||
version: "1.2.3",
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
|
|
@ -240,21 +273,18 @@ describe("AppServerClient", () => {
|
|||
it("does not notify external exit handlers for intentional disconnect exits", async () => {
|
||||
let transport!: FakeTransport;
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
|
@ -268,21 +298,18 @@ describe("AppServerClient", () => {
|
|||
it("fails active transports on transport error without waiting for exit", async () => {
|
||||
let transport!: FakeTransport;
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
|
@ -304,22 +331,19 @@ describe("AppServerClient", () => {
|
|||
it("cleans up the active transport when initialize fails", async () => {
|
||||
const transports: FakeTransport[] = [];
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
const transport = new FakeTransport(handlers);
|
||||
transports.push(transport);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const connecting = client.connect();
|
||||
const firstTransport = expectTransport(transports[0]);
|
||||
|
|
@ -353,21 +377,18 @@ describe("AppServerClient", () => {
|
|||
});
|
||||
let transport!: FakeTransport;
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const connecting = client.connect();
|
||||
const rejection = expect(connecting).rejects.toThrow("Codex app-server request timed out: initialize");
|
||||
|
|
@ -384,21 +405,18 @@ describe("AppServerClient", () => {
|
|||
it("rejects connect without notifying external exit handlers when transport exits during initialize", async () => {
|
||||
let transport!: FakeTransport;
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const connecting = client.connect();
|
||||
const rejection = expect(connecting).rejects.toThrow("Codex app-server exited: 1");
|
||||
|
|
@ -413,22 +431,19 @@ describe("AppServerClient", () => {
|
|||
const transports: FakeTransport[] = [];
|
||||
const onExit = vi.fn();
|
||||
const onNotification = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
const transport = new FakeTransport(handlers);
|
||||
transports.push(transport);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const firstConnect = client.connect();
|
||||
transports[0]?.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
|
|
@ -452,24 +467,21 @@ describe("AppServerClient", () => {
|
|||
it("ignores synchronous transport callbacks before the transport becomes active", async () => {
|
||||
let transport!: FakeTransport;
|
||||
const onExit = vi.fn();
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onExit,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
handlers.onLine(JSON.stringify({ method: "warning", params: { message: "early" } }));
|
||||
handlers.onError(new Error("early failure"));
|
||||
handlers.onExit(1, null);
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
|
|
@ -487,21 +499,18 @@ describe("AppServerClient", () => {
|
|||
});
|
||||
const logs: string[] = [];
|
||||
let transport!: FakeTransport;
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: (message) => logs.push(message),
|
||||
onExit: () => undefined,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
|
@ -526,21 +535,18 @@ describe("AppServerClient", () => {
|
|||
});
|
||||
const logs: string[] = [];
|
||||
let transport!: FakeTransport;
|
||||
const client = new AppServerClient(
|
||||
"/bin/codex",
|
||||
"/vault",
|
||||
{
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: (message) => logs.push(message),
|
||||
onExit: () => undefined,
|
||||
},
|
||||
500,
|
||||
(handlers) => {
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
);
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,26 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
|
||||
import { AppServerClient } from "../../src/app-server/connection/client";
|
||||
import {
|
||||
type AppServerClientFactory,
|
||||
ConnectionManager,
|
||||
type ConnectionManagerHandlers,
|
||||
StaleConnectionError,
|
||||
} from "../../src/app-server/connection/connection-manager";
|
||||
import type { RpcOutboundMessage } from "../../src/app-server/connection/rpc-messages";
|
||||
import type { AppServerTransport, AppServerTransportHandlers } from "../../src/app-server/connection/transport";
|
||||
import type { InitializeParams } from "../../src/generated/app-server/InitializeParams";
|
||||
|
||||
const TEST_INITIALIZE_PARAMS: InitializeParams = {
|
||||
clientInfo: {
|
||||
name: "test_client",
|
||||
title: "Test Client",
|
||||
version: "0.0.0",
|
||||
},
|
||||
capabilities: {
|
||||
experimentalApi: false,
|
||||
requestAttestation: false,
|
||||
},
|
||||
};
|
||||
|
||||
class SilentTransport implements AppServerTransport {
|
||||
readonly sent: RpcOutboundMessage[] = [];
|
||||
|
|
@ -54,11 +68,8 @@ describe("ConnectionManager", () => {
|
|||
const manager = new ConnectionManager(
|
||||
() => "/bin/codex",
|
||||
"/vault",
|
||||
(codexPath, cwd, handlers) =>
|
||||
new AppServerClient(codexPath, cwd, handlers, 5, (transportHandlers) => {
|
||||
transport = new SilentTransport(transportHandlers);
|
||||
return transport;
|
||||
}),
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ requestTimeoutMs: 5, onTransport: (next) => (transport = next) }),
|
||||
);
|
||||
|
||||
await expect(manager.connect(silentConnectionHandlers())).rejects.toThrow("Codex app-server request timed out: initialize");
|
||||
|
|
@ -72,11 +83,8 @@ describe("ConnectionManager", () => {
|
|||
const manager = new ConnectionManager(
|
||||
() => "/bin/codex",
|
||||
"/vault",
|
||||
(codexPath, cwd, handlers) =>
|
||||
new AppServerClient(codexPath, cwd, handlers, 500, (transportHandlers) => {
|
||||
transport = new SilentTransport(transportHandlers);
|
||||
return transport;
|
||||
}),
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ onTransport: (next) => (transport = next) }),
|
||||
);
|
||||
|
||||
const first = manager.connect(silentConnectionHandlers());
|
||||
|
|
@ -95,12 +103,8 @@ describe("ConnectionManager", () => {
|
|||
const manager = new ConnectionManager(
|
||||
() => codexPath,
|
||||
"/vault",
|
||||
(clientCodexPath, cwd, handlers) =>
|
||||
new AppServerClient(clientCodexPath, cwd, handlers, 500, (transportHandlers) => {
|
||||
const transport = new SilentTransport(transportHandlers);
|
||||
transports.push(transport);
|
||||
return transport;
|
||||
}),
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ onTransport: (transport) => transports.push(transport) }),
|
||||
);
|
||||
|
||||
const first = manager.connect(silentConnectionHandlers());
|
||||
|
|
@ -129,11 +133,8 @@ describe("ConnectionManager", () => {
|
|||
const manager = new ConnectionManager(
|
||||
() => "/bin/codex",
|
||||
"/vault",
|
||||
(codexPath, cwd, handlers) =>
|
||||
new AppServerClient(codexPath, cwd, handlers, 500, (transportHandlers) => {
|
||||
transport = new SilentTransport(transportHandlers);
|
||||
return transport;
|
||||
}),
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ onTransport: (next) => (transport = next) }),
|
||||
);
|
||||
|
||||
const connecting = manager.connect({ ...silentConnectionHandlers(), onExit });
|
||||
|
|
@ -150,11 +151,8 @@ describe("ConnectionManager", () => {
|
|||
const manager = new ConnectionManager(
|
||||
() => "/bin/codex",
|
||||
"/vault",
|
||||
(codexPath, cwd, handlers) =>
|
||||
new AppServerClient(codexPath, cwd, handlers, 500, (transportHandlers) => {
|
||||
transport = new SilentTransport(transportHandlers);
|
||||
return transport;
|
||||
}),
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ onTransport: (next) => (transport = next) }),
|
||||
);
|
||||
|
||||
const connecting = manager.connect({ ...silentConnectionHandlers(), onExit });
|
||||
|
|
@ -175,3 +173,22 @@ function silentConnectionHandlers(): ConnectionManagerHandlers {
|
|||
onExit: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function testClientFactory(options: {
|
||||
requestTimeoutMs?: number;
|
||||
onTransport: (transport: SilentTransport) => void;
|
||||
}): AppServerClientFactory {
|
||||
return (codexPath, cwd, handlers) =>
|
||||
new AppServerClient({
|
||||
codexPath,
|
||||
cwd,
|
||||
handlers,
|
||||
initializeParams: TEST_INITIALIZE_PARAMS,
|
||||
requestTimeoutMs: options.requestTimeoutMs ?? 500,
|
||||
transportFactory: (transportHandlers) => {
|
||||
const transport = new SilentTransport(transportHandlers);
|
||||
options.onTransport(transport);
|
||||
return transport;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
17
tests/app-server/panel-client-profile.test.ts
Normal file
17
tests/app-server/panel-client-profile.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import manifest from "../../manifest.json";
|
||||
import { codexPanelAppServerInitializeParams } from "../../src/app-server/connection/client-profile";
|
||||
import compatibility from "../../src/app-server/connection/compatibility.json";
|
||||
|
||||
describe("codexPanelAppServerInitializeParams", () => {
|
||||
it("builds the panel initialize profile from compatibility policy and manifest version", () => {
|
||||
expect(codexPanelAppServerInitializeParams()).toEqual({
|
||||
clientInfo: {
|
||||
name: "obsidian_codex_panel",
|
||||
title: "Codex Panel",
|
||||
version: manifest.version,
|
||||
},
|
||||
capabilities: compatibility.codexAppServer.initialize.capabilities,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -46,10 +46,10 @@ describe("development scripts", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("reads app-server initialize flags from the connection client in API baseline checks", async () => {
|
||||
it("reads app-server compatibility policy from the declared baseline in API baseline checks", async () => {
|
||||
const cwd = await tempWorkspace();
|
||||
await mkdir(path.join(cwd, "scripts"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src", "app-server", "connection"), { recursive: true });
|
||||
await mkdir(path.join(cwd, "src", "app-server"), { recursive: true });
|
||||
const { createApiBaselineReport } = await import(pathToFileURL(path.join(repoRoot, "scripts", "api-baseline.mjs")).href);
|
||||
|
||||
await writeJson(path.join(cwd, "package.json"), {
|
||||
|
|
@ -87,23 +87,27 @@ describe("development scripts", () => {
|
|||
path.join(cwd, "scripts", "generate-app-server-types.mjs"),
|
||||
'run("codex", ["app-server", "generate-ts", "--experimental"]);\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(cwd, "src", "app-server", "connection", "client.ts"),
|
||||
[
|
||||
'const init = await this.request("initialize", {',
|
||||
" capabilities: {",
|
||||
" experimentalApi: true,",
|
||||
" requestAttestation: false,",
|
||||
" },",
|
||||
"});",
|
||||
].join("\n"),
|
||||
);
|
||||
await mkdir(path.join(cwd, "src", "app-server", "connection"), { recursive: true });
|
||||
await writeJson(path.join(cwd, "src", "app-server", "connection", "compatibility.json"), {
|
||||
codexAppServer: {
|
||||
typeGeneration: {
|
||||
experimental: true,
|
||||
},
|
||||
initialize: {
|
||||
capabilities: {
|
||||
experimentalApi: true,
|
||||
requestAttestation: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const report = await createApiBaselineReport({
|
||||
cwd,
|
||||
readCodexVersion: () => "0.139.0",
|
||||
});
|
||||
|
||||
expect(report.codex.appServerGenerationExperimentalDeclared).toBe(true);
|
||||
expect(report.codex.initializeExperimentalApi).toBe(true);
|
||||
expect(report.codex.initializeRequestAttestationDisabled).toBe(true);
|
||||
expect(report.failures).toEqual([]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue