fix(setup): reject invalid OMP profiles before fallback writes (#1562)

This commit is contained in:
Ysopwnable 2026-10-10 22:11:14 +08:00 committed by GitHub
parent a53689c55d
commit e4cc0f09de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 638 additions and 30 deletions

View File

@ -167,6 +167,23 @@ fix for that client/model limit. It preserves the complete catalog and schemas.
Live model APIs, native Windows and default OS-home skill discovery remain
unverified.
`verify:omp-client` requires installed OMP (verified with the official Linux x64
18.8.7 binary); select it with `REA_VERIFY_OMP_COMMAND`. This optional POSIX lane
isolates the default agent, global config and XDG roots, guards setup targets,
preserves unrelated registrations and backups, and checks idempotence. It
compares native rejection of an invalid profile with REA's refusal to plan
fallback writes. OMP's default `xd://` interface mounts the complete REA catalog
as devices: call mode reads the complete installed skill and tool documentation,
then dispatches analysis through native `write`. It checks full named-schema
JavaScript Evidence with a Unicode path in the next model request. Device
metadata and documentation are distinct from forwarding all JSON schemas as
model functions. Native print-mode MCP readiness uses its defaults.
Use `-- chat` for ordinary chat or `REA_VERIFY_RUNTIME_ROOT` for a production-only
installed package. The loopback OpenAI completions/SSE model fixture declares a
one-million-token context and synthetic usage. Live model APIs, native Windows,
named-profile native execution and default OS-home skill discovery are
unverified; skill discovery uses an explicit isolated custom directory.
`verify:deepseek-client` requires an installed DeepSeek Harness (`dsh`;
verified with `@deepseek-ai/dsh@0.2.0-rc.2`) and Git. Set
`REA_VERIFY_DEEPSEEK_COMMAND` to select its executable. This optional POSIX lane

View File

@ -182,6 +182,7 @@
"verify:qwen-client": "npm run build:cached && node scripts/verify-qwen-client.mjs",
"verify:gemini-client": "npm run build:cached && node scripts/verify-gemini-client.mjs",
"verify:opencode-client": "npm run build:cached && node scripts/verify-opencode-client.mjs",
"verify:omp-client": "npm run build:cached && node scripts/verify-omp-client.mjs",
"verify:copilot-client": "npm run build:cached && node scripts/verify-copilot-client.mjs",
"verify:claude-client": "npm run build:cached && node scripts/verify-claude-client.mjs",
"verify:pi-client": "npm run build:cached && node scripts/verify-native-agent-client.mjs pi",

View File

@ -0,0 +1,441 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import {
TOOL_CONTRACTS,
toolContract,
} from "../dist/contracts/toolContracts.js";
import { exec } from "./lib/verify-package-core.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
assert(
process.platform !== "win32",
"Native OMP verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_OMP_COMMAND ?? "omp";
const mode = process.argv[2] ?? "call";
assert(
["chat", "call"].includes(mode),
"Usage: verify-omp-client.mjs [chat|call]",
);
const verifier = createVerifierRun();
const lab = await mkdtemp(join(tmpdir(), "rea-omp-client-"));
const account = join(lab, "account");
const agent = join(lab, "selected agent");
const workspace = join(lab, "workspace");
const configPath = join(agent, "mcp.json");
const skillDirectory = join(
account,
".agents",
"skills",
"reverse-engineer-anything",
);
await Promise.all([
mkdir(account),
mkdir(agent),
mkdir(workspace),
...["data", "state", "cache"].map((name) =>
mkdir(join(lab, name, "omp"), { recursive: true }),
),
]);
await exec("git", ["init", "--quiet", workspace], { timeout: 30_000 });
// OMP's global config root uses native homedir independently of the agent override.
// A documented relative PI_CONFIG_DIR redirects it without changing HOME.
const environment = {
PATH: process.env.PATH,
NODE_OPTIONS: process.env.NODE_OPTIONS,
USERPROFILE: account,
PI_CODING_AGENT_DIR: agent,
PI_CONFIG_DIR: relative(homedir(), join(lab, "native-config")),
OMP_PROFILE: "",
XDG_DATA_HOME: join(lab, "data"),
XDG_STATE_HOME: join(lab, "state"),
XDG_CACHE_HOME: join(lab, "cache"),
REA_PROCESS_RUN_ID: verifier.run_id,
};
const target = join(lab, "fixture α测试");
await mkdir(target);
await writeFile(
join(target, "app.js"),
'export function result() { return { clientCompatibility: "REA_CLIENT_FIXTURE", count: 7 }; }\n',
);
const expectedDevices = TOOL_CONTRACTS.map(
(tool) => "mcp__rea_" + tool.name,
).sort();
const analysisDevice = "xd://mcp__rea_analyze_javascript_application";
let clientVersion;
let expectedSkillBody;
let skillLoaded = false;
let contractRead = false;
let evidenceSeen = false;
let artifactDigest;
let finalSeen = false;
let catalogMounted = false;
let invalidProfileRejected = false;
let failure;
const containsText = (value, text) => {
if (typeof value === "string") return value.includes(text);
return (
value !== null &&
typeof value === "object" &&
Object.values(value).some((item) => containsText(item, text))
);
};
const call = (index, name, args) => ({
role: "assistant",
content: null,
tool_calls: [
{
id: `call_omp_${index}`,
type: "function",
function: { name, arguments: JSON.stringify(args) },
},
],
});
const fixture = await createOpenAiModelFixture({
directory: lab,
prefix: "omp",
onRequest: async ({ body, tools, requestIndex }) => {
assert(tools.some((tool) => tool.name === "read"));
assert(tools.some((tool) => tool.name === "write"));
const instructions = body.messages.filter(
(message) => message.role === "system",
);
for (const device of expectedDevices)
assert(
containsText(instructions, device),
`OMP must advertise mounted device ${device}`,
);
if (mode === "chat") {
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
}
const replies = body.messages.filter((message) => message.role === "tool");
if (requestIndex === 1)
return call(requestIndex, "read", {
i: "Load REA skill",
path: "skill://reverse-engineer-anything:raw",
});
assert(
expectedSkillBody !== undefined &&
containsText(replies, expectedSkillBody),
"OMP must return the complete installed skill body",
);
skillLoaded = true;
if (requestIndex === 2)
return call(requestIndex, "read", {
i: "Inspect REA native tool contract",
path: analysisDevice,
});
assert(containsText(replies, "# mcp__rea_analyze_javascript_application"));
assert(
containsText(replies, "input_path") &&
containsText(replies, '"directory"'),
);
contractRead = true;
if (requestIndex === 3)
return call(requestIndex, "write", {
path: analysisDevice,
content: JSON.stringify({ input_path: target, format: "directory" }),
});
const evidenceReply = replies.find(
(reply) =>
typeof reply.content === "string" &&
reply.content.startsWith('{"evidence_id":'),
);
assert(
evidenceReply,
"OMP must deliver the full analysis result to the next model turn",
);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(JSON.parse(evidenceReply.content));
assert.equal(evidence.subject.local_path, target);
const shape = evidence.normalized_result.graph.nodes
.flatMap((node) => node.observations)
.map((observation) => observation.properties)
.find(
(properties) =>
properties.semantic_role === "export-return-shapes" &&
properties.exported_name === "result",
);
assert(shape?.return_shape_coverage?.projection_complete);
const fields = shape.static_return_shapes.flatMap((item) => item.fields);
assert(
fields.some(
(field) =>
field.path === "/clientCompatibility" &&
field.state === "literal" &&
field.value === "REA_CLIENT_FIXTURE",
),
);
assert(
fields.some(
(field) =>
field.path === "/count" &&
field.state === "literal" &&
field.value === 7,
),
);
const bytes = JSON.stringify(evidence);
artifactDigest = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(lab, "omp-evidence.json"), bytes);
evidenceSeen = true;
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
},
});
const original = JSON.stringify(
{
mcpServers: { other: { command: "unrelated-server", enabled: false } },
disabledServers: ["rea", "other"],
},
null,
2,
);
await writeFile(configPath, original);
await writeFile(
join(agent, "config.yml"),
JSON.stringify({
skills: { customDirectories: [join(account, ".agents", "skills")] },
retry: { fallbackChains: { default: [] } },
}),
);
await writeFile(
join(agent, "models.yml"),
JSON.stringify({
providers: {
"rea-fixture": {
baseUrl: fixture.baseUrl,
apiKey: "local-fixture-key",
api: "openai-completions",
models: [
{
id: "rea-client-fixture",
name: "REA local fixture",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 4096,
},
],
},
},
}),
);
const rea = async (args, env = environment) =>
exec(
process.execPath,
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
{ env, cwd: workspace, timeout: 60_000 },
);
try {
clientVersion = (
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
).stdout.trim();
const invalidEnvironment = { ...environment, OMP_PROFILE: "Bad Name" };
const nativeRejected = await exec(
command,
["--print", "--no-session", "--no-title", "Hello"],
{ env: invalidEnvironment, cwd: workspace, timeout: 30_000 },
).then(
() => undefined,
(cause) => cause,
);
assert(
nativeRejected?.code === 1 &&
nativeRejected.stderr.includes('Invalid OMP profile "Bad Name"'),
);
await writeFile(
join(lab, "invalid-profile-native.stderr"),
nativeRejected.stderr,
);
const setupRejected = await rea(
["setup", "--client", "omp", "--dry-run"],
invalidEnvironment,
).then(
() => undefined,
(cause) => cause,
);
assert(typeof setupRejected?.code === "number" && setupRejected.code !== 0);
const rejected = JSON.parse(setupRejected.stdout);
assert.equal(rejected.status, "needs_human");
assert.deepEqual(rejected.plannedActions, []);
assert.deepEqual(rejected.appliedActions, []);
assert(rejected.remediation.includes('Invalid OMP profile "Bad Name"'));
assert.equal(await readFile(configPath, "utf8"), original);
await writeFile(
join(lab, "invalid-profile-setup.json"),
setupRejected.stdout,
);
invalidProfileRejected = true;
for (const args of [
["setup", "--client", "omp", "--dry-run"],
["setup", "--client", "omp", "--yes"],
["doctor", "--client", "omp", "--skill"],
]) {
const result = await rea(args);
await writeFile(
join(lab, `${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`),
result.stdout,
);
const parsed = JSON.parse(result.stdout);
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
else {
assert.equal(
parsed.status,
args.includes("--dry-run") ? "planned" : "ready",
);
if (args.includes("--dry-run")) {
assert(
parsed.plannedActions.some((action) => action.target === configPath),
);
assert(
parsed.plannedActions.some(
(action) => action.target === skillDirectory,
),
);
assert(
parsed.plannedActions.every(
(action) =>
action.target === configPath ||
action.target === skillDirectory ||
action.target.startsWith(skillDirectory + "/"),
),
"Refuse setup writes outside the owned config/skill",
);
}
}
}
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const configured = await readFile(configPath, "utf8");
const registration = JSON.parse(configured);
assert.equal(registration.mcpServers.other.command, "unrelated-server");
assert.deepEqual(registration.disabledServers, ["other"]);
const repeated = JSON.parse(
(await rea(["setup", "--client", "omp", "--yes"])).stdout,
);
assert.deepEqual(repeated.appliedActions, []);
assert.equal(await readFile(configPath, "utf8"), configured);
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
expectedSkillBody = await readFile(join(skillDirectory, "SKILL.md"), "utf8");
const execution = exec(
command,
[
"--mode",
"json",
"--print",
"--no-session",
"--no-title",
"--no-lsp",
"--no-pty",
"--approval-mode",
"yolo",
"--model",
"rea-fixture/rea-client-fixture",
mode === "chat"
? "Say hello briefly."
: `Use the REA skill to analyze ${target}.`,
],
{
env: environment,
cwd: workspace,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
},
);
execution.child.stdin.end();
const response = await execution;
await writeFile(join(lab, `${mode}.stdout`), response.stdout);
await writeFile(join(lab, `${mode}.stderr`), response.stderr);
assert.equal(fixture.failure, undefined);
const events = response.stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
const mounted = events
.filter(
(event) =>
event.type === "notice" && event.message?.startsWith("xd://: mounted "),
)
.flatMap((event) =>
event.message.slice("xd://: mounted ".length).split(", "),
)
.filter((name) => name.startsWith("mcp__rea_"));
assert.deepEqual(
mounted.sort(),
expectedDevices,
"Native OMP must mount the complete REA catalog",
);
catalogMounted = true;
assert(
!events.some(
(event) => event.type === "tool_execution_end" && event.isError,
),
);
assert(
events.some(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message.stopReason === "stop" &&
containsText(event.message.content, "REA_CLIENT_COMPATIBILITY_OK"),
),
"OMP must finish with the expected native assistant message",
);
assert(
events.some((event) => event.type === "agent_end" && event.isTerminal),
);
assert(finalSeen);
if (mode === "call") assert(skillLoaded && contractRead && evidenceSeen);
} catch (cause) {
failure = cause;
if (typeof cause?.stdout === "string")
await writeFile(join(lab, `${mode}.stdout`), cause.stdout);
if (typeof cause?.stderr === "string")
await writeFile(join(lab, `${mode}.stderr`), cause.stderr);
} finally {
await fixture.close();
const lineage = await completeVerifierRun(verifier);
const receipt = {
client: "OMP",
clientVersion,
mode,
passed: failure === undefined,
artifacts: lab,
runtimeRoot,
host: {
platform: process.platform,
architecture: process.arch,
node: process.versions.node,
},
modelFixture:
"loopback OpenAI completions/SSE, caller-declared one-million-token model and synthetic usage; no live model claim",
discovery:
"explicit isolated default agent/global config/XDG roots and custom skill directory; named profiles, default OS-home skill discovery and Windows unverified",
interface:
"default native xd:// device metadata with read/write dispatch; complete JSON schemas are not forwarded as model functions",
catalogMounted,
catalogSize: expectedDevices.length,
invalidProfileRejected,
skillLoaded,
contractRead,
evidenceSeen,
artifactDigest,
finalSeen,
requests: fixture.requests,
probes: fixture.probes,
fixtureFailure: fixture.failure,
verifier: lineage,
};
await writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2));
console.log(JSON.stringify(receipt));
}
if (failure !== undefined) throw failure;

View File

@ -1,6 +1,7 @@
import { dirname, isAbsolute, join, resolve, win32 } from "node:path";
import { fileURLToPath } from "node:url";
import { lstatSync } from "node:fs";
import { err, ok, type Result } from "../domain/result.js";
import {
HermesClientPathError,
resolveHermesClientHome,
@ -39,7 +40,15 @@ export interface SetupClient {
| "unsupported";
}
type ClientPath = readonly string[] | ((paths: ClientPathContext) => string);
interface ClientPathFailure {
readonly path: string;
readonly message: string;
}
type ClientPathResult = Result<string, ClientPathFailure>;
type ClientPath =
| readonly string[]
| ((paths: ClientPathContext) => string | ClientPathResult);
interface ClientPathContext {
readonly home: string;
@ -146,34 +155,32 @@ export const manualRegistrationRemediation = (
: undefined;
const OMP_PROFILE_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
/**
* OMP's active named profile. OMP_PROFILE wins over the legacy PI_PROFILE even
* when empty; empty and "default" select the default profile. OMP refuses to
* start with an invalid name, so one leaves the default location in place.
*/
const ompProfile = ({ env }: ClientPathContext): string | undefined => {
const name = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim();
return name === undefined ||
name === "" ||
name === "default" ||
name.endsWith(".") ||
!OMP_PROFILE_NAME.test(name)
? undefined
: name;
};
const OMP_RESERVED_PROFILE =
/^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/iu;
/**
* The OMP agent directory whose `mcp.json` holds user-scope MCP servers. OMP
* joins PI_CONFIG_DIR to the home directory, and a named profile ignores
* PI_CODING_AGENT_DIR. Relative overrides use the setup process working directory.
*/
const ompAgentDirectory = (context: ClientPathContext): string => {
const ompAgentDirectory = (context: ClientPathContext): ClientPathResult => {
const root = join(context.home, context.env.PI_CONFIG_DIR || ".omp");
const profile = ompProfile(context);
if (profile !== undefined) return join(root, "profiles", profile, "agent");
// OMP_PROFILE wins even when empty. Match OMP's validation on every host.
const profile = (context.env.OMP_PROFILE ?? context.env.PI_PROFILE)?.trim();
if (profile !== undefined && profile !== "" && profile !== "default") {
if (
!OMP_PROFILE_NAME.test(profile) ||
profile.endsWith(".") ||
OMP_RESERVED_PROFILE.test(profile)
)
return err({
path: root,
message: `Invalid OMP profile ${JSON.stringify(profile)}: names must match ^[a-z0-9][a-z0-9._-]{0,63}$, cannot end with ".", and cannot be a Windows reserved device name (CON, PRN, AUX, NUL, COM0-9, LPT0-9, including extensions). Repair OMP_PROFILE or PI_PROFILE before configuring OMP.`,
});
return ok(join(root, "profiles", profile, "agent"));
}
const override = context.env.PI_CODING_AGENT_DIR;
return override ? resolve(override) : join(root, "agent");
return ok(override ? resolve(override) : join(root, "agent"));
};
/**
@ -456,8 +463,10 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
{
name: "omp",
displayName: "OMP",
configPath: (context: ClientPathContext) =>
join(ompAgentDirectory(context), "mcp.json"),
configPath: (context: ClientPathContext) => {
const directory = ompAgentDirectory(context);
return directory.ok ? ok(join(directory.value, "mcp.json")) : directory;
},
markerPath: ompAgentDirectory,
format: "omp",
},
@ -508,8 +517,14 @@ export const clientEvidencePaths = (
),
];
const resolvePath = (path: ClientPath, context: ClientPathContext): string =>
typeof path === "function" ? path(context) : join(context.home, ...path);
const resolvePath = (
path: ClientPath,
context: ClientPathContext,
): ClientPathResult => {
const resolved =
typeof path === "function" ? path(context) : join(context.home, ...path);
return typeof resolved === "string" ? ok(resolved) : resolved;
};
/** Describe every client location that setup, doctor, or uninstall may inspect. */
export const supportedClients = (
@ -521,7 +536,27 @@ export const supportedClients = (
const definitions: readonly ClientDefinition[] = SUPPORTED_CLIENT_DEFINITIONS;
return definitions.map((definition) => {
try {
const configPath = resolvePath(definition.configPath, context);
const config = resolvePath(definition.configPath, context);
const marker = config.ok
? resolvePath(definition.markerPath, context)
: config;
if (!config.ok)
return {
name: definition.name,
displayName: definition.displayName,
format: definition.format,
configPath: config.error.path,
configPathError: config.error.message,
};
if (!marker.ok)
return {
name: definition.name,
displayName: definition.displayName,
format: definition.format,
configPath: marker.error.path,
configPathError: marker.error.message,
};
const configPath = config.value;
return {
name: definition.name,
displayName: definition.displayName,
@ -536,7 +571,7 @@ export const supportedClients = (
...new Set([...definition.configPaths(context), configPath]),
],
}),
markerPath: resolvePath(definition.markerPath, context),
markerPath: marker.value,
format: definition.format,
};
} catch (cause: unknown) {
@ -584,14 +619,16 @@ export const clientSkillDirectories = (
const definition = definitions.find(({ name }) => name === client);
let directory: string;
try {
directory =
const resolved =
definition?.skillPath === undefined
? join(home, ".agents", "skills")
? ok(join(home, ".agents", "skills"))
: resolvePath(definition.skillPath, {
home,
platform,
env: environment,
});
if (!resolved.ok) continue;
directory = resolved.value;
} catch (cause: unknown) {
// Registration diagnostics own an unresolved profile; never fall back to another skill root.
if (cause instanceof HermesClientPathError) continue;

View File

@ -0,0 +1,112 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
// Match native profile rules, including reserved names on POSIX.
for (const [variable, profile] of [
["OMP_PROFILE", "Bad Name"],
["OMP_PROFILE", "../escape"],
["OMP_PROFILE", "trailing."],
["OMP_PROFILE", "con.txt"],
["OMP_PROFILE", "com0"],
["OMP_PROFILE", "lpt0"],
["OMP_PROFILE", "a".repeat(65)],
["PI_PROFILE", "Bad Name"],
] as const)
cliTest(
`OMP rejects invalid ${variable} ${profile} without writing a fallback`,
async ({ cli }) => {
const home = await createTestTempDirectory("rea-omp-invalid-profile-");
const agent = join(home, "selected agent");
const config = join(agent, "mcp.json");
const original = '{"mcpServers":{"other":{"command":"other-server"}}}\n';
await mkdir(agent);
await writeFile(config, original);
const run = (args: readonly string[], profile: string) =>
cli.run({
arguments: args,
cwd: home,
environment: {
USERPROFILE: home,
PI_CODING_AGENT_DIR: agent,
[variable]: profile,
},
timeoutMs: 20_000,
});
for (const option of ["--dry-run", "--yes"]) {
const rejected = await run(
["setup", "--client", "omp", option, "--json"],
profile,
);
expect(rejected.exitCode).not.toBe(0);
expect(rejected.json).toMatchObject({
status: "needs_human",
plannedActions: [],
appliedActions: [],
remediation: expect.stringContaining(
`Invalid OMP profile ${JSON.stringify(profile)}`,
),
});
}
expect(await readFile(config, "utf8")).toBe(original);
for (const path of [
config + ".rea.backup",
join(home, ".omp", "agent"),
join(home, ".agents", "skills"),
])
await expect(access(path)).rejects.toMatchObject({ code: "ENOENT" });
if (profile !== "con.txt") return;
const doctor = await run(
["doctor", "--client", "omp", "--json"],
"con.txt",
);
expect(doctor.exitCode).not.toBe(0);
expect(doctor.json).toMatchObject({
identity: {
registrations: expect.arrayContaining([
expect.objectContaining({
client: "omp",
state: "invalid",
remediation: expect.stringContaining(
"Windows reserved device name",
),
}),
]),
},
});
const unrelated = await run(
["setup", "--client", "qwen_code", "--yes", "--json"],
"Bad Name",
);
expect(unrelated.exitCode).toBe(0);
expect(unrelated.json).toMatchObject({ status: "ready" });
},
);
cliTest(
"OMP explicit default selection overrides an invalid legacy profile",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-omp-default-profile-");
const agent = join(home, "agent");
const result = await cli.run({
arguments: ["setup", "--client", "omp", "--yes", "--json"],
cwd: home,
environment: {
USERPROFILE: home,
PI_CODING_AGENT_DIR: agent,
OMP_PROFILE: " ",
PI_PROFILE: "Bad Name",
},
timeoutMs: 20_000,
});
expect(result.exitCode).toBe(0);
expect(result.json).toMatchObject({ status: "ready" });
expect(
JSON.parse(await readFile(join(agent, "mcp.json"), "utf8")),
).toHaveProperty("mcpServers.rea");
},
);

View File

@ -56,7 +56,7 @@ describe("OMP configuration paths", () => {
expect(ompPath({ OMP_PROFILE: "", PI_PROFILE: "legacy" })?.configPath).toBe(
"/home/a/.omp/agent/mcp.json",
);
for (const profile of ["default", " ", "Bad Name", "trailing."])
for (const profile of ["default", " "])
expect(ompPath({ OMP_PROFILE: profile })?.configPath).toBe(
"/home/a/.omp/agent/mcp.json",
);