diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 7f5d636fba..31509da61e 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -283,12 +283,6 @@ "src/apktool/ApktoolConfiguration.ts": 4, "src/apktool/ApktoolProvider.ts": 2, "src/application/BinaryTargetResolver.ts": 1, - "src/application/ClientConfigPath.ts": 1, - "src/application/ClientConfigurationFiles.ts": 4, - "src/application/ClientConfigurationPublication.ts": 1, - "src/application/ClientRegistrationEnvironment.ts": 4, - "src/application/JsonFiles.ts": 1, - "src/application/SupportedClients.ts": 2, "src/application/binary/AnalysisSnapshotCache.ts": 1, "src/application/binaryDiagnostics/ByteEntropyService.ts": 1, "src/application/desktop/DesktopBuildComputers.ts": 1, diff --git a/src/application/ClientConfigPath.ts b/src/application/ClientConfigPath.ts index b2490ae1bf..03478ae5ed 100644 --- a/src/application/ClientConfigPath.ts +++ b/src/application/ClientConfigPath.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { lstat, realpath } from "node:fs/promises"; import { basename, dirname, join } from "node:path"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; @@ -75,7 +76,7 @@ const canonicalRegularConfigPath = async ( if (fileSystem.realpath === undefined) return requestedPath; let ancestor = absoluteCallerPath(requestedPath); const directoryRequired = - process.platform !== "win32" && ancestor.endsWith("/"); + HOST_PLATFORM.platform !== "win32" && ancestor.endsWith("/"); const missing: string[] = []; while (true) { try { diff --git a/src/application/ClientConfigurationDocument.capacity.test.ts b/src/application/ClientConfigurationDocument.capacity.test.ts index 2fdb58ed5f..b873539c8f 100644 --- a/src/application/ClientConfigurationDocument.capacity.test.ts +++ b/src/application/ClientConfigurationDocument.capacity.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { describe, expect, it } from "vitest"; import { @@ -33,10 +34,14 @@ describe("TOML configuration source capacity", () => { newline, ); const parsed = parseClientConfiguration(original, "toml"); - const desired = withClientServers(parsed, { - ...parsed.servers, - rea: registration, - }); + const desired = withClientServers( + parsed, + { + ...parsed.servers, + rea: registration, + }, + HOST_PLATFORM.platform, + ); const configured = serializedText(desired, "toml", original); expect( configured.startsWith("\uFEFF" + prefix.replaceAll("\n", newline)), @@ -45,7 +50,7 @@ describe("TOML configuration source capacity", () => { desired, ); const removed = serializedText( - withClientServers(parsed, {}), + withClientServers(parsed, {}, HOST_PLATFORM.platform), "toml", configured, ); @@ -61,10 +66,14 @@ describe("TOML configuration source capacity", () => { const note = `first\n${"x\n".repeat(500_000)}last`; const original = `mcp_servers = { other = {command="other",env={NOTE="""${note}"""}}}\n`; const parsed = parseClientConfiguration(original, "toml"); - const desired = withClientServers(parsed, { - ...parsed.servers, - rea: registration, - }); + const desired = withClientServers( + parsed, + { + ...parsed.servers, + rea: registration, + }, + HOST_PLATFORM.platform, + ); const configured = serializedText(desired, "toml", original); expect(configured).toContain(`NOTE="""${note}"""`); expect(parseClientConfiguration(configured, "toml").document).toEqual( @@ -83,10 +92,14 @@ describe("TOML configuration source capacity", () => { `disabled_mcp_servers = [\n"rea",\n${otherNames}]\n` + '[mcp_servers.other]\ncommand = "other"\n'; const parsed = parseClientConfiguration(original, "grok"); - const desired = withClientServers(parsed, { - ...parsed.servers, - rea: registration, - }); + const desired = withClientServers( + parsed, + { + ...parsed.servers, + rea: registration, + }, + HOST_PLATFORM.platform, + ); const configured = serializedText(desired, "grok", original); const result = parseClientConfiguration(configured, "grok"); expect(result.document.disabled_mcp_servers).toEqual( diff --git a/src/application/ClientConfigurationDocument.jsonRetention.test.ts b/src/application/ClientConfigurationDocument.jsonRetention.test.ts index e1f2044152..6cd268bcb7 100644 --- a/src/application/ClientConfigurationDocument.jsonRetention.test.ts +++ b/src/application/ClientConfigurationDocument.jsonRetention.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { expect, it } from "vitest"; import { @@ -13,10 +14,14 @@ const replaceLauncher = (source: string, format: ClientConfigurationFormat) => { const existing = parsed.servers.rea; if (typeof existing !== "object" || existing === null) throw new Error("fixture registration missing"); - const document = withClientServers(parsed, { - ...parsed.servers, - rea: { ...existing, command: "new-rea" }, - }); + const document = withClientServers( + parsed, + { + ...parsed.servers, + rea: { ...existing, command: "new-rea" }, + }, + HOST_PLATFORM.platform, + ); const result = serializeClientConfiguration(document, format, source, [ clientServerPath(parsed, "rea"), ]); @@ -67,13 +72,17 @@ it("retains nested array tokens while changing siblings, shrinking and extending const source = `{"mcpServers":{"rea":{"command":"rea","items":[{"opaque":9007199254740993,"label":"old"},2,3],"grow":[9007199254740993]}}}`; const parsed = parseClientConfiguration(source, "json"); const result = serializeClientConfiguration( - withClientServers(parsed, { - rea: { - command: "rea", - items: [{ opaque: 9007199254740992, label: "new" }], - grow: [9007199254740992, 4], + withClientServers( + parsed, + { + rea: { + command: "rea", + items: [{ opaque: 9007199254740992, label: "new" }], + grow: [9007199254740992, 4], + }, }, - }), + HOST_PLATFORM.platform, + ), "json", source, [clientServerPath(parsed, "rea")], @@ -90,7 +99,11 @@ it("retains source tokens when the caller supplies a whole edited document", () const source = `{"opaque":9007199254740993,"mcpServers":{"rea":{"command":"old"}}}`; const parsed = parseClientConfiguration(source, "fx"); const result = serializeClientConfiguration( - withClientServers(parsed, { rea: { command: "new" } }), + withClientServers( + parsed, + { rea: { command: "new" } }, + HOST_PLATFORM.platform, + ), "fx", source, ); @@ -103,13 +116,17 @@ it("removes all repeated nested fields without activating an earlier value", () const source = `{"mcpServers":{"rea":{"remove":1,"remove":2,"retain":9007199254740993,"command":"old"}}}`; const parsed = parseClientConfiguration(source, "json"); const result = serializeClientConfiguration( - withClientServers(parsed, { - rea: { - retain: 9007199254740992, - command: "new", - added: { enabled: true }, + withClientServers( + parsed, + { + rea: { + retain: 9007199254740992, + command: "new", + added: { enabled: true }, + }, }, - }), + HOST_PLATFORM.platform, + ), "json", source, [clientServerPath(parsed, "rea")], @@ -132,7 +149,7 @@ it.each([ const source = `{"mcpServers":{"rea":{"items":[1,2,],"old":3,}}}`; const parsed = parseClientConfiguration(source, "fx"); const result = serializeClientConfiguration( - withClientServers(parsed, { rea }), + withClientServers(parsed, { rea }, HOST_PLATFORM.platform), "fx", source, [clientServerPath(parsed, "rea")], diff --git a/src/application/ClientConfigurationDocument.repeatedMembers.test.ts b/src/application/ClientConfigurationDocument.repeatedMembers.test.ts index d79cedaf70..ab8c119b29 100644 --- a/src/application/ClientConfigurationDocument.repeatedMembers.test.ts +++ b/src/application/ClientConfigurationDocument.repeatedMembers.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { describe, expect, it } from "vitest"; import { parse as parseJsonc, type ParseError } from "jsonc-parser"; @@ -21,7 +22,7 @@ const removeRea = ( const parsed = parseClientConfiguration(text, format); const { rea: _removed, ...remaining } = parsed.servers; return serializeClientConfiguration( - withClientServers(parsed, remaining), + withClientServers(parsed, remaining, HOST_PLATFORM.platform), format, text, [clientServerPath(parsed, "rea")], @@ -72,7 +73,7 @@ describe("JSON client configuration repeated-member edits", () => { it("does not apply repeat-removal authority to a different path", () => { const text = `{"mcpServers":{"rea":1,"rea":2,"peer":3,"peer":4}}`; const parsed = parseClientConfiguration(text, "json"); - const document = withClientServers(parsed, {}); + const document = withClientServers(parsed, {}, HOST_PLATFORM.platform); const result = serializeClientConfiguration( document, "json", diff --git a/src/application/ClientConfigurationDocument.tomlComments.test.ts b/src/application/ClientConfigurationDocument.tomlComments.test.ts index 0905e4c7a8..3bb3852425 100644 --- a/src/application/ClientConfigurationDocument.tomlComments.test.ts +++ b/src/application/ClientConfigurationDocument.tomlComments.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { expect, it } from "vitest"; import { @@ -38,7 +39,7 @@ for (const format of ["toml", "grok"] as const) ? { ...peers, rea: { command: "new", args: [] } } : peers; const result = serializeClientConfiguration( - withClientServers(parsed, servers), + withClientServers(parsed, servers, HOST_PLATFORM.platform), format, source, ); @@ -80,7 +81,7 @@ it("retains comments from dotted REA assignments and nested inline values", () = const { rea: removed, ...servers } = parsed.servers; expect(removed).toBeDefined(); const result = serializeClientConfiguration( - withClientServers(parsed, servers), + withClientServers(parsed, servers, HOST_PLATFORM.platform), "toml", source, ); @@ -111,7 +112,7 @@ it.each(["toml", "grok"] as const)( rea: { ...previous, command: "new" }, }; const result = serializeClientConfiguration( - withClientServers(parsed, servers), + withClientServers(parsed, servers, HOST_PLATFORM.platform), format, source, ); diff --git a/src/application/ClientConfigurationDocument.ts b/src/application/ClientConfigurationDocument.ts index cdcccf309d..272e1c9c43 100644 --- a/src/application/ClientConfigurationDocument.ts +++ b/src/application/ClientConfigurationDocument.ts @@ -532,22 +532,24 @@ export const clientServerForEdit = ( /** * Return `parsed.document` with `servers` at its server table. A legacy OpenCode - * V1 member named in `removeLegacy` is removed beside a native V2 table. + * V1 member named in `removeLegacy` is removed beside a native V2 table. The + * platform names the environment identity a Cordis entry is written with. */ export const withClientServers = ( parsed: ClientConfigurationDocument, servers: Record, + platform: NodeJS.Platform, removeLegacy?: string, ): Record => { // The Harness patch list upserts or removes by entry id, not by table. if (parsed.dialect === "dsh") return Object.hasOwn(servers, PRODUCT_IDENTITY.mcpServerKey) - ? editDshDocument( - parsed, - clientConfigurationRecordSchema.parse( + ? editDshDocument(parsed, { + desired: clientConfigurationRecordSchema.parse( servers[PRODUCT_IDENTITY.mcpServerKey], ), - ) + platform, + }) : editDshDocument(parsed); if (parsed.serversPath.length === 0) return { ...servers }; const [key = "", nested] = parsed.serversPath; diff --git a/src/application/ClientConfigurationFiles.ts b/src/application/ClientConfigurationFiles.ts index 0844784c7e..07503baccf 100644 --- a/src/application/ClientConfigurationFiles.ts +++ b/src/application/ClientConfigurationFiles.ts @@ -123,8 +123,8 @@ export const readConfigurationFile = async ( /** Read each layer; setup may validate its writable layer with proposed overrides. */ export const readClientConfigurationFiles = async ( client: SetupClient, - resources?: ArtifactResourceScope, - platform: NodeJS.Platform = process.platform, + resources: ArtifactResourceScope | undefined, + platform: NodeJS.Platform, writeOverrides?: Readonly>, ): Promise< Result @@ -156,9 +156,9 @@ export const readClientConfigurationFiles = async ( const environment = clientRegistrationEnvironment( document.value, client, + platform, path === client.configPath ? writeOverrides : undefined, "fragment", - platform, ); if (!environment.ok) return err( @@ -182,9 +182,9 @@ export const readClientConfigurationFiles = async ( const environment = clientRegistrationEnvironment( effectiveClientConfiguration(documents, undefined, platform), client, + platform, writeOverrides, "effective", - platform, ); if (!environment.ok) return err( @@ -249,11 +249,13 @@ export const clientConfigurationForWrite = ( /** Project a client's effective user registration without modifying its source files. */ export const effectiveClientConfiguration = ( sources: readonly ClientConfigurationSource[], - target?: { - readonly client: Pick; - readonly document: ClientConfigurationDocument | undefined; - }, - platform: NodeJS.Platform = process.platform, + target: + | { + readonly client: Pick; + readonly document: ClientConfigurationDocument | undefined; + } + | undefined, + platform: NodeJS.Platform, ): ClientConfigurationDocument | undefined => { const documents = target === undefined @@ -389,7 +391,7 @@ export const launchedRegistrationEntry = ( /** Merge admitted layers without re-reading or changing their source documents. */ export const mergeClientConfigurationDocuments = ( documents: readonly ClientConfigurationDocument[], - platform: NodeJS.Platform = process.platform, + platform: NodeJS.Platform, ): ClientConfigurationDocument | undefined => { const last = documents.at(-1); if (last === undefined) return undefined; @@ -439,8 +441,8 @@ export const mergeClientConfigurationDocuments = ( /** Read the configuration ZCode will load, preserving its same-scope fallback rule. */ export const readEffectiveClientRegistrationConfiguration = async ( client: SetupClient, - resources?: ArtifactResourceScope, - platform: NodeJS.Platform = process.platform, + resources: ArtifactResourceScope | undefined, + platform: NodeJS.Platform, ): Promise< Result< { diff --git a/src/application/ClientConfigurationPublication.ts b/src/application/ClientConfigurationPublication.ts index 6b91af1e99..07f97a70cf 100644 --- a/src/application/ClientConfigurationPublication.ts +++ b/src/application/ClientConfigurationPublication.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { constants, type Stats } from "node:fs"; import { link, lstat, mkdtemp, open, realpath, rename } from "node:fs/promises"; import { basename, dirname, join, resolve, sep } from "node:path"; @@ -124,7 +125,7 @@ export class ClientConfigurationPublication { if (selection?.requiredMode !== undefined) { await owner.handle.chmod(selection.requiredMode); if ( - process.platform !== "win32" && + HOST_PLATFORM.platform !== "win32" && ((await owner.handle.stat()).mode & 0o777) !== selection.requiredMode ) throw new ClientConfigurationWriteError( diff --git a/src/application/ClientRegistration.test.ts b/src/application/ClientRegistration.test.ts index 86350707c1..db8b713172 100644 --- a/src/application/ClientRegistration.test.ts +++ b/src/application/ClientRegistration.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { expect, it } from "vitest"; import { @@ -215,6 +216,7 @@ it.each([ name: "goose", registrationProfile: supportedClientRegistrationProfile("goose"), }, + HOST_PLATFORM.platform, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Null Goose env alias was admitted"); diff --git a/src/application/ClientRegistrationEnvironment.ts b/src/application/ClientRegistrationEnvironment.ts index 13ba7b1d02..f43bcc26a6 100644 --- a/src/application/ClientRegistrationEnvironment.ts +++ b/src/application/ClientRegistrationEnvironment.ts @@ -99,16 +99,16 @@ const expressionSettings = ( export const clientRegistrationEnvironment = ( parsed: ClientConfigurationDocument | undefined, client: Pick | undefined, + platform: NodeJS.Platform, overrides: Readonly> = {}, validation: "fragment" | "effective" = "effective", - platform: NodeJS.Platform = process.platform, ): Result>, ConfigurationError> => { const details = clientRegistrationEnvironmentDetails( parsed, client, + platform, overrides, validation, - platform, ); return details.ok ? ok(details.value.values) : details; }; @@ -117,9 +117,9 @@ export const clientRegistrationEnvironment = ( export const clientRegistrationEnvironmentDetails = ( parsed: ClientConfigurationDocument | undefined, client: Pick | undefined, + platform: NodeJS.Platform, overrides: Readonly> = {}, validation: "fragment" | "effective" = "effective", - platform: NodeJS.Platform = process.platform, ): Result => { if (parsed?.dialect === "dsh") { const environment = dshDocument(parsed.document).environment( @@ -207,7 +207,7 @@ export const recordedProviderSettings = ( parsed: ClientConfigurationDocument, client: Pick | undefined, selected: Readonly>, - platform: NodeJS.Platform = process.platform, + platform: NodeJS.Platform, owns?: OwnedProviderSetting, observe?: ProviderSettingObserver, ): RecordedProviderSettings | undefined => { @@ -219,9 +219,9 @@ export const recordedProviderSettings = ( const details = clientRegistrationEnvironmentDetails( parsed, client, + platform, {}, "fragment", - platform, ); if (!details.ok) return { unrecorded: [], differing: [], unresolved: names }; const recorded = snapshotEnvironment(details.value.values, platform); @@ -273,16 +273,16 @@ export const clientRegistrationEnvironmentDetailsWithBudget = async ( overrides: Readonly>, validation: "fragment" | "effective", resolveProviderRequestBudget: ProviderRequestBudgetResolver | undefined, - platform: NodeJS.Platform = process.platform, + platform: NodeJS.Platform, ): Promise< Result > => { const details = clientRegistrationEnvironmentDetails( parsed, client, + platform, overrides, validation, - platform, ); if (!details.ok) return details; const unresolved = [...details.value.unresolvedSettings].filter( diff --git a/src/application/DshCordisPatch.ts b/src/application/DshCordisPatch.ts index 4cfffeb6f2..b56afc52a0 100644 --- a/src/application/DshCordisPatch.ts +++ b/src/application/DshCordisPatch.ts @@ -1,5 +1,4 @@ import { validateRegistrationEnvironment } from "./RegistrationEnvironmentValidation.js"; -import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { isMap, isSeq, type Node, type YAMLMap } from "yaml"; import type { Environment } from "../config/environment.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; @@ -282,7 +281,8 @@ export class DshCordisDocument { } /** Plan every replacement from source snapshots before changing an output tree. */ - serialize(desired?: Record): string { + serialize(edit?: DshCordisRegistration): string { + const desired = edit?.desired; if ( this.#editabilityError !== undefined && (this.#unknownStructure || desired !== undefined) @@ -319,9 +319,11 @@ export class DshCordisDocument { const owned = new Set(op.ownedRows); inserted.items = inserted.items.flatMap((entry, index) => { if (!owned.has(index)) return [entry]; - if (carried || desired === undefined) return []; + if (carried || edit === undefined) return []; carried = true; - return [this.#entry(desired, invalidated)] as typeof inserted.items; + return [ + this.#entry(edit.desired, edit.platform, invalidated), + ] as typeof inserted.items; }); if (inserted.items.length === 0) copy.delete("insert"); // An emptied operation keeps caller fields unless it would patch REA's row. @@ -356,6 +358,7 @@ export class DshCordisDocument { #entry( desired: Record, + platform: NodeJS.Platform, invalidated: ReadonlySet, ): YAMLMap { const entry = this.#yaml.detach(this.#entryNode, invalidated); @@ -395,9 +398,10 @@ export class DshCordisDocument { this.#yaml.renameKey(nextEnv, pair, key); continue; } - // Serialization rewrites this host's file, as setup's write path does. + // Windows environment names match case-insensitively, so a caller's + // spelling of a desired name is kept rather than duplicated. const alias = - HOST_PLATFORM.platform === "win32" + platform === "win32" ? Object.keys(desiredEnv).find( (name) => name.toUpperCase() === key.toUpperCase(), ) @@ -415,10 +419,16 @@ export class DshCordisDocument { } } +/** REA's desired entry and the platform whose environment names it uses. */ +export interface DshCordisRegistration { + readonly desired: Record; + readonly platform: NodeJS.Platform; +} + class DshCordisEdit { constructor( readonly source: DshCordisDocument, - readonly desired?: Record, + readonly registration?: DshCordisRegistration, ) {} } export const dshDocument = ( @@ -443,16 +453,16 @@ export const parseDshCordisDocument = ( }; export const editDshDocument = ( parsed: ClientConfigurationDocument, - desired?: Record, + registration?: DshCordisRegistration, ): Record => ({ - cordis: new DshCordisEdit(dshDocument(parsed.document), desired), + cordis: new DshCordisEdit(dshDocument(parsed.document), registration), }); export const serializeDshDocument = ( document: Record, ): string => { if (!(document.cordis instanceof DshCordisEdit)) throw new ConfigurationError("Missing Cordis edit plan"); - return document.cordis.source.serialize(document.cordis.desired); + return document.cordis.source.serialize(document.cordis.registration); }; export const dshClientRegistrationEntry = ( command: readonly string[], diff --git a/src/application/HermesConfigurationYaml.test.ts b/src/application/HermesConfigurationYaml.test.ts index 1b07b013eb..2dccd2a153 100644 --- a/src/application/HermesConfigurationYaml.test.ts +++ b/src/application/HermesConfigurationYaml.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { @@ -626,10 +627,14 @@ it("retains exact integer settings through a parsed Hermes launcher refresh", () const parsed = parseClientConfiguration(source, "hermes"); const previous = parsed.servers.rea as Record; const result = serializeClientConfiguration( - withClientServers(parsed, { - ...parsed.servers, - rea: { ...previous, command: "rea" }, - }), + withClientServers( + parsed, + { + ...parsed.servers, + rea: { ...previous, command: "rea" }, + }, + HOST_PLATFORM.platform, + ), "hermes", source, ); @@ -812,9 +817,10 @@ describe("Hermes registration refresh through inherited merges", () => { it("keeps an inherited stdio transport through a launcher refresh", () => { const source = "defaults: &defaults\n transport: stdio\n command: rea\n args: [mcp]\nmcp_servers:\n rea:\n <<: *defaults\n"; - const client = supportedClients("/home/rea-test").find( - ({ name }) => name === "hermes", - )!; + const client = supportedClients( + "/home/rea-test", + HOST_PLATFORM.platform, + ).find(({ name }) => name === "hermes")!; const parsed = parseClientConfiguration(source, "hermes"); const registration = refreshClientRegistration( client, @@ -830,7 +836,11 @@ describe("Hermes registration refresh through inherited merges", () => { ); if (!registration.ok) throw registration.error; const written = serializeClientConfiguration( - withClientServers(parsed, { ...parsed.servers, rea: registration.value }), + withClientServers( + parsed, + { ...parsed.servers, rea: registration.value }, + HOST_PLATFORM.platform, + ), "hermes", source, ); diff --git a/src/application/JsonFiles.ts b/src/application/JsonFiles.ts index f5143f99e8..25549a1ef5 100644 --- a/src/application/JsonFiles.ts +++ b/src/application/JsonFiles.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { link, lstat, @@ -378,7 +379,7 @@ const writeTextPartsOwned = async ( const requestedPath = absoluteCallerPath(path); try { assertWriteActive(cancellation); - if (process.platform !== "win32" && requestedPath.endsWith("/")) + if (HOST_PLATFORM.platform !== "win32" && requestedPath.endsWith("/")) return err( new EvidenceFileError("write", "not-file", { path: requestedPath }), ); diff --git a/src/application/McpClientRegistration.layers.test.ts b/src/application/McpClientRegistration.layers.test.ts index ed7351c817..2420e750a2 100644 --- a/src/application/McpClientRegistration.layers.test.ts +++ b/src/application/McpClientRegistration.layers.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { lstat, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { afterEach, beforeEach, expect, it } from "vitest"; @@ -50,6 +51,7 @@ const effective = ( ) => { const parsed = mergeClientConfigurationDocuments( texts.map((text) => parseClientConfiguration(text, format)), + HOST_PLATFORM.platform, ); if (parsed === undefined) throw new Error("Fixture has no configuration documents"); diff --git a/src/application/McpRegistrationConfiguration.ts b/src/application/McpRegistrationConfiguration.ts index 444fe9381e..22c73e1820 100644 --- a/src/application/McpRegistrationConfiguration.ts +++ b/src/application/McpRegistrationConfiguration.ts @@ -1,6 +1,7 @@ import { ConfigurationError } from "../domain/configurationErrors.js"; import type { ClientRegistrationCommand } from "../domain/clientRegistrationCommand.js"; import { PRODUCT_IDENTITY } from "../identity.js"; +import { HOST_PLATFORM } from "../platform/HostPlatform.js"; import { clientConfigurationValuesEqual, clientServerPath, @@ -23,6 +24,9 @@ import { admitClientStdioConnection } from "./ClientRegistrationConnection.js"; import type { McpRegistrationClient } from "./McpRegistrationCatalog.js"; import { readRegularFileTextSnapshot } from "./RegularFileRead.js"; +// `rea mcp add` writes and verifies this host's client files. +const { platform } = HOST_PLATFORM; + const isMissing = (cause: unknown): boolean => cause instanceof Error && "code" in cause && cause.code === "ENOENT"; @@ -53,6 +57,7 @@ export const prepareRegistration = async ( const targetIndex = paths.lastIndexOf(client.configPath); const inherited = mergeClientConfigurationDocuments( layers.slice(0, targetIndex), + platform, ); const desired = replaceClientRegistrationLauncher( { dialect: parsed.dialect, profile: client.registrationProfile }, @@ -84,6 +89,7 @@ export const prepareRegistration = async ( ...parsed.servers, [PRODUCT_IDENTITY.mcpServerKey]: desired.value, }, + platform, PRODUCT_IDENTITY.mcpServerKey, ); const legacy = legacyClientServerPath(parsed, PRODUCT_IDENTITY.mcpServerKey); @@ -162,7 +168,7 @@ const assertEffectiveLauncher = ( layers: readonly ClientConfigurationDocument[], launcher: ClientRegistrationCommand, ): void => { - const effective = mergeClientConfigurationDocuments(layers); + const effective = mergeClientConfigurationDocuments(layers, platform); const admitted = admitClientStdioConnection( effective === undefined ? undefined diff --git a/src/application/SetupClientConfiguration.ts b/src/application/SetupClientConfiguration.ts index ae1fb19953..4aa9b916cf 100644 --- a/src/application/SetupClientConfiguration.ts +++ b/src/application/SetupClientConfiguration.ts @@ -109,6 +109,8 @@ export const configureClientConfiguration = async ( environment: SetupProviderEnvironment = {}, command: readonly string[] = currentClientRegistrationCommand(), resolveProviderRequestBudget?: ProviderRequestBudgetResolver, + // Setup writes the running host's client files unless its host selects one. + platform: NodeJS.Platform = HOST_PLATFORM.platform, ): Promise => { if (command.length === 0) return { @@ -127,7 +129,7 @@ export const configureClientConfiguration = async ( try { return await withClientConfigurationTransaction(transactionPath, () => configureClientDocument( - { client, transactionPath }, + { client, transactionPath, platform }, environment, command, resolveProviderRequestBudget, @@ -148,7 +150,12 @@ const configureClientDocument = async ( { client, transactionPath, - }: { readonly client: SetupClient; readonly transactionPath: string }, + platform, + }: { + readonly client: SetupClient; + readonly transactionPath: string; + readonly platform: NodeJS.Platform; + }, environment: SetupProviderEnvironment, command: readonly string[], resolveProviderRequestBudget?: ProviderRequestBudgetResolver, @@ -159,7 +166,7 @@ const configureClientDocument = async ( const files = await readClientConfigurationFiles( client, undefined, - undefined, + platform, environment, ); if (!files.ok) { @@ -204,6 +211,7 @@ const configureClientDocument = async ( parsed, original ?? "", files.value, + platform, ); if (ownershipBlocker !== undefined) return { @@ -220,6 +228,7 @@ const configureClientDocument = async ( command, parsed, files.value, + platform, resolveProviderRequestBudget, ), ); @@ -235,7 +244,7 @@ const configureClientDocument = async ( void cause; return { status: "failed", reason: "readback" }; } - const fallback = await readZcodeFallbackServers(client, parsed); + const fallback = await readZcodeFallbackServers(client, platform, parsed); if (!fallback.ok) { if (fallback.error.kind !== "registration_ownership") throwCleanupFailure(fallback.error); @@ -256,11 +265,17 @@ const configureClientDocument = async ( servers: { ...fallback.value, ...parsed.servers }, }, files.value, + platform, resolveProviderRequestBudget, ); // The settings written now can differ from the ones inspection admitted, // for example a Hopper path set by an installation earlier in this setup. - const override = inlineOverrideBlocker(client, files.value, desired); + const override = inlineOverrideBlocker( + client, + files.value, + desired, + platform, + ); if (override !== undefined) return { status: "failed", reason: "read_only_override", detail: override }; if ( @@ -277,6 +292,7 @@ const configureClientDocument = async ( parsed, desired, fallbackServers: fallback.value, + platform, }); }; @@ -291,6 +307,7 @@ interface ClientConfigurationUpdate { readonly parsed: ClientConfigurationDocument; readonly desired: unknown; readonly fallbackServers: Readonly>; + readonly platform: NodeJS.Platform; } const persistClientConfiguration = async ({ @@ -302,6 +319,7 @@ const persistClientConfiguration = async ({ parsed, desired, fallbackServers, + platform, }: ClientConfigurationUpdate): Promise => { // The plan chose this file; an MSIX package may have switched to its // private copy since, for example when it quit. @@ -335,6 +353,7 @@ const persistClientConfiguration = async ({ ...parsed.servers, [PRODUCT_IDENTITY.mcpServerKey]: desired, }, + platform, PRODUCT_IDENTITY.mcpServerKey, ); // OMP's denylist would hide the registration; keep every other listed name. @@ -409,6 +428,7 @@ const persistClientConfiguration = async ({ ? { mode: PRIVATE_PROFILE_MODES.directory } : {}), }); + // File modes are this host's, whatever platform the registration targets. if ( client.privateProfile !== undefined && HOST_PLATFORM.platform !== "win32" @@ -519,20 +539,22 @@ const readClientRefresh = async ( providerEnvironment: SetupProviderEnvironment, command: readonly string[], { + platform, resolveProviderRequestBudget, resources, currentCommandPath, }: { + readonly platform: NodeJS.Platform; readonly resolveProviderRequestBudget?: ProviderRequestBudgetResolver; readonly resources?: ArtifactResourceScope; /** The entry point whose registrations setup owns; update names the new one. */ readonly currentCommandPath?: string; - } = {}, + }, ): Promise> => { const files = await readClientConfigurationFiles( client, resources, - undefined, + platform, providerEnvironment, ); if (!files.ok) { @@ -561,6 +583,7 @@ const readClientRefresh = async ( command, parsed, files.value, + platform, resolveProviderRequestBudget, ); const target = @@ -570,6 +593,7 @@ const readClientRefresh = async ( files.value, parsed, original, + platform, currentCommandPath, ); if (target !== undefined) @@ -584,7 +608,7 @@ const readClientRefresh = async ( client, files.value, desired, - HOST_PLATFORM.platform, + platform, ); if (overrideOrigin !== undefined) return ok({ @@ -626,6 +650,8 @@ export const clientConfigurationAligned = async ( providerEnvironment: SetupProviderEnvironment, command: readonly string[], resolveProviderRequestBudget?: ProviderRequestBudgetResolver, + // Setup reads the running host's client files unless its host selects one. + platform: NodeJS.Platform = HOST_PLATFORM.platform, ): Promise => { if (client.configPathError !== undefined) return false; try { @@ -634,8 +660,8 @@ export const clientConfigurationAligned = async ( providerEnvironment, command, resolveProviderRequestBudget === undefined - ? {} - : { resolveProviderRequestBudget }, + ? { platform } + : { platform, resolveProviderRequestBudget }, ); return ( refresh.ok && @@ -698,6 +724,7 @@ export const clientRefreshRefusal = async ( providerEnvironment, command, { + platform, currentCommandPath, ...(resources === undefined ? {} : { resources }), }, @@ -782,6 +809,7 @@ const discoveredInlineSettings = async ( command, refresh.parsed, refresh.sources, + platform, ); return inlineRegistrationOverride( client, @@ -799,6 +827,8 @@ export const inspectClientConfiguration = async ( providerEnvironment: SetupProviderEnvironment, command: readonly string[], resolveProviderRequestBudget?: ProviderRequestBudgetResolver, + // Setup reads the running host's client files unless its host selects one. + platform: NodeJS.Platform = HOST_PLATFORM.platform, ): Promise => { let inspection: ClientConfigurationInspection; try { @@ -806,6 +836,7 @@ export const inspectClientConfiguration = async ( client, providerEnvironment, command, + platform, resolveProviderRequestBudget, ); } catch (cause: unknown) { @@ -828,6 +859,7 @@ const inspectClientDocument = async ( client: SetupClient, providerEnvironment: SetupProviderEnvironment, command: readonly string[], + platform: NodeJS.Platform, resolveProviderRequestBudget?: ProviderRequestBudgetResolver, ): Promise => { if (client.configPathError !== undefined) @@ -836,7 +868,7 @@ const inspectClientDocument = async ( const files = await readClientConfigurationFiles( client, undefined, - undefined, + platform, providerEnvironment, ); if (!files.ok) { @@ -869,6 +901,7 @@ const inspectClientDocument = async ( files.value, undefined, undefined, + platform, ); if (ownershipBlocker !== undefined) return { status: "invalid", remediation: ownershipBlocker }; @@ -878,9 +911,15 @@ const inspectClientDocument = async ( command, undefined, files.value, + platform, resolveProviderRequestBudget, ); - const override = inlineOverrideBlocker(client, files.value, desired); + const override = inlineOverrideBlocker( + client, + files.value, + desired, + platform, + ); if (override !== undefined) return { status: "invalid", remediation: override }; // This read establishes absence; do not reuse an earlier user-file policy. @@ -894,7 +933,7 @@ const inspectClientDocument = async ( } if (policy.value !== undefined) return { status: "invalid", remediation: policy.value }; - const fallback = await readZcodeFallbackServers(client); + const fallback = await readZcodeFallbackServers(client, platform); if (!fallback.ok) { if (fallback.error.kind !== "registration_ownership") throwCleanupFailure(fallback.error); @@ -917,6 +956,7 @@ const inspectClientDocument = async ( files.value, parsed, original, + platform, ); if (ownershipBlocker !== undefined) return { status: "invalid", remediation: ownershipBlocker }; @@ -926,9 +966,15 @@ const inspectClientDocument = async ( command, parsed, files.value, + platform, resolveProviderRequestBudget, ); - const override = inlineOverrideBlocker(client, files.value, desired); + const override = inlineOverrideBlocker( + client, + files.value, + desired, + platform, + ); if (override !== undefined) return { status: "invalid", remediation: override }; const policy = await readClientPolicyBlock(client, parsed, desired); @@ -946,7 +992,7 @@ const inspectClientDocument = async ( (await privateProfileCurrent(client, original)) ) return { status: "already_current" }; - const fallback = await readZcodeFallbackServers(client, parsed); + const fallback = await readZcodeFallbackServers(client, platform, parsed); if (!fallback.ok) { if (fallback.error.kind !== "registration_ownership") throwCleanupFailure(fallback.error); @@ -993,14 +1039,9 @@ const inlineOverrideBlocker = ( client: SetupClient, sources: readonly ClientConfigurationSource[], desired: unknown, + platform: NodeJS.Platform, ): string | undefined => { - // Setup writes and its client reads this file on the running host. - const origin = inlineRegistrationOverride( - client, - sources, - desired, - HOST_PLATFORM.platform, - ); + const origin = inlineRegistrationOverride(client, sources, desired, platform); return origin === undefined ? undefined : inlineOverrideRemediation(client, origin); @@ -1034,6 +1075,7 @@ const writeTargetOwnershipBlocker = ( sources: readonly ClientConfigurationSource[], parsed: ClientConfigurationDocument | undefined, original: string | undefined, + platform: NodeJS.Platform, currentCommandPath?: string, ): string | undefined => { if (parsed !== undefined) @@ -1042,12 +1084,14 @@ const writeTargetOwnershipBlocker = ( parsed, original ?? "", sources, + platform, currentCommandPath, ); - const inherited = effectiveClientConfiguration(sources, { - client, - document: undefined, - }); + const inherited = effectiveClientConfiguration( + sources, + { client, document: undefined }, + platform, + ); return inherited === undefined ? undefined : registrationOwnershipBlocker( @@ -1055,6 +1099,7 @@ const writeTargetOwnershipBlocker = ( inherited, "", sources, + platform, currentCommandPath, ); }; @@ -1064,17 +1109,19 @@ const registrationOwnershipBlocker = ( client: SetupClient, parsed: ClientConfigurationDocument, original: string, - sources: readonly ClientConfigurationSource[] = [], + sources: readonly ClientConfigurationSource[], + platform: NodeJS.Platform, currentCommandPath?: string, ): string | undefined => { // Cordis owns registrations by stable entry ID, including malformed launchers. if (parsed.dialect === "dsh") return undefined; const name = PRODUCT_IDENTITY.mcpServerKey; const entries: { value: unknown; paths: readonly string[] }[] = []; - const effective = effectiveClientConfiguration(sources, { - client, - document: parsed, - }); + const effective = effectiveClientConfiguration( + sources, + { client, document: parsed }, + platform, + ); if (effective !== undefined && Object.hasOwn(effective.servers, name)) entries.push({ value: effective.servers[name], @@ -1121,6 +1168,7 @@ const registrationOwnershipBlocker = ( /** Admit the fallback only when the first native ZCode server would suppress it. */ const readZcodeFallbackServers = async ( client: SetupClient, + platform: NodeJS.Platform, parsed?: ClientConfigurationDocument, ): Promise< Result< @@ -1155,6 +1203,8 @@ const readZcodeFallbackServers = async ( { ...client, configPath: path, format: "zcode_fallback" }, parsed, source.value, + [], + platform, ); if (ownershipBlocker !== undefined) return err({ @@ -1162,7 +1212,7 @@ const readZcodeFallbackServers = async ( path, detail: ownershipBlocker, }); - const environment = clientRegistrationEnvironment(parsed, client); + const environment = clientRegistrationEnvironment(parsed, client, platform); if (!environment.ok) return err({ kind: "malformed", @@ -1262,7 +1312,11 @@ const registrationCurrent = ( !Object.hasOwn(parsed.legacyServers, PRODUCT_IDENTITY.mcpServerKey) && !clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey); -/** A private profile with the wrong modes needs publication even when its row is current. */ +/** + * A private profile with the wrong modes needs publication even when its row + * is current. Modes belong to the host holding the file, not to the platform + * the registration is written for. + */ const privateProfileCurrent = async ( client: SetupClient, original: string | undefined, @@ -1280,12 +1334,14 @@ const clientConfigurationDesired = async ( command: readonly string[], parsed: ClientConfigurationDocument | undefined, sources: readonly ClientConfigurationSource[], + platform: NodeJS.Platform, resolveProviderRequestBudget?: ProviderRequestBudgetResolver, ): Promise => { - const effective = effectiveClientConfiguration(sources, { - client, - document: parsed, - }); + const effective = effectiveClientConfiguration( + sources, + { client, document: parsed }, + platform, + ); const format = clientWriteDialect( parsed?.dialect ?? client.format ?? "json", sources, @@ -1296,6 +1352,7 @@ const clientConfigurationDesired = async ( const validated = clientRegistrationEnvironment( effective, client, + platform, providerEnvironment, ); if (!validated.ok) throw validated.error; @@ -1303,7 +1360,7 @@ const clientConfigurationDesired = async ( parsed === undefined ? new DshCordisDocument("") : dshDocument(parsed.document) - ).desired(launcher, providerEnvironment, HOST_PLATFORM.platform); + ).desired(launcher, providerEnvironment, platform); if (!desired.ok) throw desired.error; return desired.value; } @@ -1315,6 +1372,7 @@ const clientConfigurationDesired = async ( client.name === "codex" || client.name === "kimi" ? resolveProviderRequestBudget : undefined, + platform, ); if (!validated.ok) throw validated.error; // Preserve explicitly configured server settings, never the ambient process @@ -1323,6 +1381,7 @@ const clientConfigurationDesired = async ( const environment = clientRegistrationEnvironment( parsed, client, + platform, providerEnvironment, "fragment", ); @@ -1346,6 +1405,7 @@ const clientConfigurationDesired = async ( sources, effective, providerEnvironment, + platform, ); }; @@ -1371,13 +1431,15 @@ const withInheritedGooseSettings = ( sources: readonly ClientConfigurationSource[], effective: ClientConfigurationDocument | undefined, providerEnvironment: SetupProviderEnvironment, + platform: NodeJS.Platform, ): Record => { if (format !== "goose") return withoutInherited; // The layers below the user file, which the user entry overlays field by field. - const lower = effectiveClientConfiguration(sources, { - client, - document: undefined, - }); + const lower = effectiveClientConfiguration( + sources, + { client, document: undefined }, + platform, + ); const declares = ( document: ClientConfigurationDocument | undefined, keys: readonly string[], @@ -1422,6 +1484,7 @@ const withInheritedGooseSettings = ( const inherited = clientRegistrationEnvironment( effective, client, + platform, providerEnvironment, "fragment", ); diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index 7902150ec1..3351b30977 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -303,6 +303,7 @@ export const systemSetupHost = ( ...clientConfigurationOperations( resources, doctorHost.resolveProviderRequestBudget, + platform, ), skillNeedsInstall: (clientIds) => canonicalSkillNeedsInstall( @@ -363,6 +364,7 @@ const setupInstallerDiagnostic = ( const clientConfigurationOperations = ( resources: ArtifactResourceScope, resolveProviderRequestBudget: DoctorHost["resolveProviderRequestBudget"], + platform: NodeJS.Platform, ) => ({ configureClient: ( client: SetupClient, @@ -377,6 +379,7 @@ const clientConfigurationOperations = ( providerEnvironment, command, resolveProviderRequestBudget, + platform, ), ), clientNeedsConfigure: ( @@ -392,6 +395,7 @@ const clientConfigurationOperations = ( providerEnvironment, command, resolveProviderRequestBudget, + platform, )), ), inspectClientConfiguration: ( @@ -405,6 +409,7 @@ const clientConfigurationOperations = ( providerEnvironment, command, resolveProviderRequestBudget, + platform, ), ), }); diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts index e8ac607b9a..4c703dedfe 100644 --- a/src/application/SupportedClients.ts +++ b/src/application/SupportedClients.ts @@ -2006,7 +2006,7 @@ const layerLoadOrder = ( /** Describe every client location that setup, doctor, or uninstall may inspect. */ export const supportedClients = ( home: string, - platform: NodeJS.Platform = process.platform, + platform: NodeJS.Platform, env: ClientPathContext["env"] = process.env, systemPaths: ClientSystemPaths = {}, ): readonly SetupClient[] => { @@ -2136,8 +2136,8 @@ export const resolveSupportedClients = async ( export const clientSkillDirectories = ( home: string, clientIds: readonly string[] | undefined, - environment: Readonly = {}, - platform: NodeJS.Platform = process.platform, + environment: Readonly, + platform: NodeJS.Platform, ): { readonly directories: readonly { readonly client: string; diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index 49e27adb94..39d42ae7fe 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -550,10 +550,8 @@ export const systemUninstallHost = ( if (upper !== undefined) lowerLayers.push(upper); return { kind: "entry", - entry: - effectiveClientConfiguration(lowerLayers)?.servers[ - PRODUCT_IDENTITY.mcpServerKey - ], + entry: effectiveClientConfiguration(lowerLayers, undefined, platform) + ?.servers[PRODUCT_IDENTITY.mcpServerKey], }; }; /** @@ -574,9 +572,11 @@ export const systemUninstallHost = ( const inlineEntry = inline === undefined ? undefined - : effectiveClientConfiguration([ - { ...inline.parsed, inlineOrigin: inline.origin }, - ])?.servers[PRODUCT_IDENTITY.mcpServerKey]; + : effectiveClientConfiguration( + [{ ...inline.parsed, inlineOrigin: inline.origin }], + undefined, + platform, + )?.servers[PRODUCT_IDENTITY.mcpServerKey]; if ( inline !== undefined && enabledRegistration(inlineEntry) && @@ -820,14 +820,16 @@ export const systemUninstallHost = ( originalLayers.push(layer); remainingLayers.push(layer); } - const selected = - effectiveClientConfiguration(originalLayers)?.servers[ - PRODUCT_IDENTITY.mcpServerKey - ]; - const remaining = - effectiveClientConfiguration(remainingLayers)?.servers[ - PRODUCT_IDENTITY.mcpServerKey - ]; + const selected = effectiveClientConfiguration( + originalLayers, + undefined, + platform, + )?.servers[PRODUCT_IDENTITY.mcpServerKey]; + const remaining = effectiveClientConfiguration( + remainingLayers, + undefined, + platform, + )?.servers[PRODUCT_IDENTITY.mcpServerKey]; const altersEffectiveRegistration = clientRegistrationOwnership(selected, client) === "owned" ? remaining !== undefined @@ -878,6 +880,7 @@ export const systemUninstallHost = ( client, fileSystem, resources, + platform, () => clientWriteBlocker(client), preflightOwnedClients.has(client), ), @@ -1212,6 +1215,7 @@ const removeClient = async ( client: UninstallClient, fileSystem: UninstallFileSystem, resources: ArtifactResourceScope, + platform: NodeJS.Platform, writeBlocker?: () => string | undefined, preflightOwned = false, ): Promise => { @@ -1252,6 +1256,7 @@ const removeClient = async ( const document = withClientServers( parsed, remaining, + platform, removeLegacy ? name : undefined, ); const editedPaths = [ diff --git a/src/doctorRuntime.ts b/src/doctorRuntime.ts index c55b7946c2..5770ffd003 100644 --- a/src/doctorRuntime.ts +++ b/src/doctorRuntime.ts @@ -162,9 +162,9 @@ const readSelectedRegistration = async ( const details = clientRegistrationEnvironmentDetails( configuration.value.document, client, + platform, {}, "effective", - platform, ); return details.ok ? { diff --git a/src/mcpStartupPolicy.test.ts b/src/mcpStartupPolicy.test.ts index 877372f5fc..ceff84ca84 100644 --- a/src/mcpStartupPolicy.test.ts +++ b/src/mcpStartupPolicy.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "./platform/HostPlatform.js"; import { describe, expect, it } from "vitest"; import { parseClientConfiguration } from "./application/ClientConfigurationDocument.js"; @@ -55,6 +56,7 @@ describe("MCP tool deadline allowance for a Ghidra decompile limit", () => { "json", ), undefined, + HOST_PLATFORM.platform, ); if (!details.ok) throw details.error; expect(details.value.timeoutInputs).toMatchObject({ diff --git a/tests/boundary/filesystem/clientConfigurationDialects.test.ts b/tests/boundary/filesystem/clientConfigurationDialects.test.ts index e3729ede18..5580160324 100644 --- a/tests/boundary/filesystem/clientConfigurationDialects.test.ts +++ b/tests/boundary/filesystem/clientConfigurationDialects.test.ts @@ -33,7 +33,7 @@ beforeEach(clearClientLocationEnvironment); afterEach(() => vi.unstubAllEnvs()); const getClients = (home: string) => - supportedClients(home).filter(({ name }) => + supportedClients(home, HOST_PLATFORM.platform).filter(({ name }) => [ "opencode", "antigravity", @@ -57,7 +57,9 @@ describe("additional client configuration dialects", () => { if (CURRENT_ENTRYPOINT_PATH === undefined) throw new Error("test runner entrypoint is unavailable"); const home = await createTestTempDirectory("rea-relative-uninstall-"); - const cursor = supportedClients(home).find(({ name }) => name === "cursor"); + const cursor = supportedClients(home, HOST_PLATFORM.platform).find( + ({ name }) => name === "cursor", + ); if (cursor === undefined) throw new Error("missing Cursor client"); await mkdir(cursor.markerPath ?? home, { recursive: true }); const relativeScript = relative(process.cwd(), CURRENT_ENTRYPOINT_PATH); @@ -323,7 +325,9 @@ it.each([ expectedComment, }) => { const home = await createTestTempDirectory("rea-cursor-jsonc-remove-"); - const client = supportedClients(home).find(({ name }) => name === "cursor"); + const client = supportedClients(home, HOST_PLATFORM.platform).find( + ({ name }) => name === "cursor", + ); if (client === undefined) throw new Error("missing Cursor client"); await mkdir(client.markerPath ?? home, { recursive: true }); diff --git a/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts b/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts index 98c6998c47..1a981445ad 100644 --- a/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts +++ b/tests/boundary/filesystem/clientConfigurationEnvironment.test.ts @@ -56,6 +56,7 @@ it("awaits the owner-parsed provider request budget without changing caller stri expect(provider).toBe("ida"); return ok(17); }, + HOST_PLATFORM.platform, ); expect(details).toMatchObject({ ok: true, @@ -202,6 +203,7 @@ it.skipIf(process.platform !== "win32").each([ const env = clientRegistrationEnvironmentDetails( parseClientConfiguration(text, "dsh"), undefined, + HOST_PLATFORM.platform, ); if (!env.ok) throw env.error; expect(env.value.values).toHaveProperty(override, value); @@ -295,6 +297,7 @@ it const details = clientRegistrationEnvironmentDetails( parseClientConfiguration(text, "dsh"), undefined, + HOST_PLATFORM.platform, ); if (!details.ok) throw details.error; expect(details.value.timeoutInputs?.analysisProvider).toBe("ghidra"); @@ -327,6 +330,7 @@ it.skipIf(process.platform !== "win32").each([ const result = clientRegistrationEnvironmentDetails( parsed, undefined, + HOST_PLATFORM.platform, overrides, ); if (!result.ok) throw result.error; @@ -489,9 +493,9 @@ it.each(["linux", "win32"] as const)( const details = clientRegistrationEnvironmentDetails( parseClientConfiguration(source, "dsh"), undefined, + platform, {}, "effective", - platform, ); if (!details.ok) throw details.error; expect(Object.entries(details.value.values)).toEqual([ @@ -520,15 +524,21 @@ it("retains an unresolved JEB endpoint expression until a selected literal repla {}, "effective", undefined, + HOST_PLATFORM.platform, ); if (!unresolved.ok) throw unresolved.error; expect(unresolved.value.unresolvedSettings).toEqual( new Set(["REA_JEB_MCP_URL"]), ); const endpoint = "http://127.0.0.1:8426/mcp/"; - const selected = clientRegistrationEnvironmentDetails(parsed, undefined, { - REA_JEB_MCP_URL: endpoint, - }); + const selected = clientRegistrationEnvironmentDetails( + parsed, + undefined, + HOST_PLATFORM.platform, + { + REA_JEB_MCP_URL: endpoint, + }, + ); if (!selected.ok) throw selected.error; expect(selected.value.values.REA_JEB_MCP_URL).toBe(endpoint); expect(selected.value.unresolvedSettings.size).toBe(0); @@ -561,9 +571,9 @@ it.each( const values = clientRegistrationEnvironment( parsed, undefined, + platform, {}, "effective", - platform, ); expect(values).toMatchObject({ ok: true, @@ -572,9 +582,9 @@ it.each( const details = clientRegistrationEnvironmentDetails( parsed, undefined, + platform, {}, "effective", - platform, ); if (!details.ok) throw details.error; expect(details.value.values).toEqual({ CALLER_SETTING: "retained" }); @@ -600,11 +610,11 @@ it.each( const overridden = clientRegistrationEnvironmentDetails( parsed, undefined, + platform, { [setting]: setting === "REA_ANALYSIS_PROVIDER" ? "ghidra" : "17000", }, "effective", - platform, ); if (!overridden.ok) throw overridden.error; expect(overridden.value.unresolvedSettings.size).toBe(0); @@ -625,7 +635,11 @@ it("retains an unknown companion while deriving unrelated literal timeout facts" `, "dsh", ); - const details = clientRegistrationEnvironmentDetails(parsed, undefined); + const details = clientRegistrationEnvironmentDetails( + parsed, + undefined, + HOST_PLATFORM.platform, + ); if (!details.ok) throw details.error; expect(details.value.unresolvedSettings).toEqual( new Set(["REA_GHIDRA_LANGUAGE_ID"]), @@ -660,6 +674,7 @@ it("does not resolve a provider budget from an expression omitted from literal v {}, "effective", undefined, + HOST_PLATFORM.platform, ), ).toMatchObject({ ok: true, @@ -672,6 +687,7 @@ it("does not resolve a provider budget from an expression omitted from literal v {}, "effective", resolveBudget, + HOST_PLATFORM.platform, ), ).toMatchObject({ ok: false, @@ -684,6 +700,7 @@ it("does not resolve a provider budget from an expression omitted from literal v { REA_IDA_MCP_CONFIG: "/selected/ida.json" }, "effective", resolveBudget, + HOST_PLATFORM.platform, ); expect(resolved).toMatchObject({ ok: true, @@ -701,6 +718,7 @@ it("rejects a non-string caller environment key before editing configuration", a const details = clientRegistrationEnvironmentDetails( parseClientConfiguration(source, "json"), undefined, + HOST_PLATFORM.platform, ); expect(details).toMatchObject({ ok: false, @@ -1015,10 +1033,14 @@ it("treats a variable-expanding client's recorded expression as unresolved", () }), "json", ); - const qwen = clientRegistrationEnvironmentDetails(parsed, { - name: "qwen_code", - registrationProfile: supportedClientRegistrationProfile("qwen_code"), - }); + const qwen = clientRegistrationEnvironmentDetails( + parsed, + { + name: "qwen_code", + registrationProfile: supportedClientRegistrationProfile("qwen_code"), + }, + HOST_PLATFORM.platform, + ); if (!qwen.ok) throw qwen.error; // Qwen expands the expression from its own environment, so REA records the // name as unresolved rather than treating the literal text as the value. @@ -1026,10 +1048,14 @@ it("treats a variable-expanding client's recorded expression as unresolved", () expect(qwen.value.values.PATH).toBe("${PATH}:/opt/jdk/bin"); // A client that does not expand expressions keeps the same text as a value. - const cursor = clientRegistrationEnvironmentDetails(parsed, { - name: "cursor", - registrationProfile: supportedClientRegistrationProfile("cursor"), - }); + const cursor = clientRegistrationEnvironmentDetails( + parsed, + { + name: "cursor", + registrationProfile: supportedClientRegistrationProfile("cursor"), + }, + HOST_PLATFORM.platform, + ); if (!cursor.ok) throw cursor.error; expect(cursor.value.unresolvedSettings).toEqual(new Set()); expect(cursor.value.values.PATH).toBe("${PATH}:/opt/jdk/bin"); @@ -1048,10 +1074,14 @@ it("keeps a typed Qwen setting unresolved without discarding its recorded expres }), "json", ); - const details = clientRegistrationEnvironmentDetails(parsed, { - name: "qwen_code", - registrationProfile: supportedClientRegistrationProfile("qwen_code"), - }); + const details = clientRegistrationEnvironmentDetails( + parsed, + { + name: "qwen_code", + registrationProfile: supportedClientRegistrationProfile("qwen_code"), + }, + HOST_PLATFORM.platform, + ); expect(details.ok).toBe(true); if (!details.ok) return; expect(details.value.values.REA_GHIDRA_STARTUP_TIMEOUT_MS).toBe("${JOBS}"); @@ -1079,9 +1109,9 @@ it("keeps a differently cased Qwen expression unresolved on Windows", () => { name: "qwen_code", registrationProfile: supportedClientRegistrationProfile("qwen_code"), }, + "win32", {}, "fragment", - "win32", ); expect(details.ok).toBe(true); if (!details.ok) return; @@ -1114,10 +1144,14 @@ it.each([ }), "json", ); - const details = clientRegistrationEnvironmentDetails(parsed, { - name, - registrationProfile: supportedClientRegistrationProfile(name), - }); + const details = clientRegistrationEnvironmentDetails( + parsed, + { + name, + registrationProfile: supportedClientRegistrationProfile(name), + }, + HOST_PLATFORM.platform, + ); if (name === "cline" && typeof value !== "string") { expect(details).toMatchObject({ ok: false, diff --git a/tests/boundary/filesystem/clientConfigurationLayeredEnvironment.test.ts b/tests/boundary/filesystem/clientConfigurationLayeredEnvironment.test.ts index e9fb1988f1..0ae9f38e6f 100644 --- a/tests/boundary/filesystem/clientConfigurationLayeredEnvironment.test.ts +++ b/tests/boundary/filesystem/clientConfigurationLayeredEnvironment.test.ts @@ -78,9 +78,17 @@ it.each(["missing", "empty", "legacy", "native"] as const)( expect( await configureClientConfiguration(client, {}, command), ).toMatchObject({ status: "configured" }); - const files = await readClientConfigurationFiles(client); + const files = await readClientConfigurationFiles( + client, + undefined, + HOST_PLATFORM.platform, + ); if (!files.ok) throw new Error(files.error.detail); - const effective = effectiveClientConfiguration(files.value); + const effective = effectiveClientConfiguration( + files.value, + undefined, + HOST_PLATFORM.platform, + ); expect(effective?.dialect).toBe("opencode_v2"); expect( effective === undefined @@ -165,11 +173,20 @@ it.skipIf(process.platform !== "win32").each([ command, ), ).toMatchObject({ status: "configured" }); - const sources = await readClientConfigurationFiles(fixture.client); + const sources = await readClientConfigurationFiles( + fixture.client, + undefined, + HOST_PLATFORM.platform, + ); if (!sources.ok) throw new Error(sources.error.detail); const environment = clientRegistrationEnvironmentDetails( - effectiveClientConfiguration(sources.value), + effectiveClientConfiguration( + sources.value, + undefined, + HOST_PLATFORM.platform, + ), undefined, + HOST_PLATFORM.platform, ); if (!environment.ok) throw environment.error; expect(environment.value.timeoutInputs?.analysisProvider).toBe("ghidra"); @@ -289,10 +306,14 @@ it("merges caller server names as own keys without inventing absent registration "opencode", ); expect(effectiveClientServer(lower, "constructor")).toBeUndefined(); - const effective = effectiveClientConfiguration([ - { ...lower, sourcePath: "lower.json" }, - { ...upper, sourcePath: "upper.json" }, - ]); + const effective = effectiveClientConfiguration( + [ + { ...lower, sourcePath: "lower.json" }, + { ...upper, sourcePath: "upper.json" }, + ], + undefined, + HOST_PLATFORM.platform, + ); expect(effective).toBeDefined(); if (effective === undefined) throw new Error("missing layered configuration"); expect(Object.keys(effective.servers)).toEqual(["__proto__"]); diff --git a/tests/boundary/filesystem/clientConfigurationOverrideOrigin.test.ts b/tests/boundary/filesystem/clientConfigurationOverrideOrigin.test.ts index 075056f028..87c2e576bd 100644 --- a/tests/boundary/filesystem/clientConfigurationOverrideOrigin.test.ts +++ b/tests/boundary/filesystem/clientConfigurationOverrideOrigin.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js"; import { join } from "node:path"; import { writeFile } from "node:fs/promises"; import { expect, it } from "vitest"; @@ -47,12 +48,16 @@ it("keeps a malformed stored environment attributed to its file", async () => { }, }), ); - const result = await readClientConfigurationFiles({ - name: "cursor", - registrationProfile: supportedClientRegistrationProfile("cursor"), - format: "json", - configPath, - }); + const result = await readClientConfigurationFiles( + { + name: "cursor", + registrationProfile: supportedClientRegistrationProfile("cursor"), + format: "json", + configPath, + }, + undefined, + HOST_PLATFORM.platform, + ); expect(result).toMatchObject({ ok: false, error: { detail: expect.stringContaining(`Configuration ${configPath}`) }, diff --git a/tests/boundary/filesystem/clientLifecycle.test.ts b/tests/boundary/filesystem/clientLifecycle.test.ts index 2a6fbb84c8..56fc4a0f0b 100644 --- a/tests/boundary/filesystem/clientLifecycle.test.ts +++ b/tests/boundary/filesystem/clientLifecycle.test.ts @@ -92,7 +92,7 @@ const DETECTED_CLIENT_ORDER = [ describe("client configuration filesystem lifecycle", () => { it("detects every supported client and skips absent clients", async () => { const home = await createTestTempDirectory("rea-detect-"); - for (const client of supportedClients(home)) + for (const client of supportedClients(home, HOST_PLATFORM.platform)) if (client.markerPath !== undefined) await mkdir(client.markerPath, { recursive: true }); // Gemini CLI follows its own session state, not the shared .gemini root. @@ -109,7 +109,7 @@ describe("client configuration filesystem lifecycle", () => { it("does not detect Gemini CLI from Antigravity's shared .gemini storage", async () => { const home = await createTestTempDirectory("rea-detect-gemini-"); - const antigravity = supportedClients(home).find( + const antigravity = supportedClients(home, HOST_PLATFORM.platform).find( ({ name }) => name === "antigravity", ); if (antigravity?.markerPath === undefined) @@ -1563,7 +1563,7 @@ describe("Grok Bot registration", () => { const original = '{"mcpServers":{"other":{"command":"other"},"rea":{"command":"rea","args":["mcp"]}}}\n'; await writeFile(configPath, original); - const client = supportedClients(home).find( + const client = supportedClients(home, HOST_PLATFORM.platform).find( ({ name }) => name === "grok_bot", ); if (client === undefined) throw new Error("missing grok_bot"); diff --git a/tests/boundary/filesystem/dshClientConfiguration.test.ts b/tests/boundary/filesystem/dshClientConfiguration.test.ts index 04c5f9ddc0..c402443887 100644 --- a/tests/boundary/filesystem/dshClientConfiguration.test.ts +++ b/tests/boundary/filesystem/dshClientConfiguration.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js"; import { currentClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; @@ -98,7 +99,12 @@ describe("DeepSeek Harness configuration paths", () => { markerPath: join(home, dshRoot), }); expect( - clientSkillDirectories(home, ["dsh"], environment).directories, + clientSkillDirectories( + home, + ["dsh"], + environment, + HOST_PLATFORM.platform, + ).directories, ).toEqual([{ client, directory: join(home, agentsRoot, "skills") }]); }, ); @@ -118,7 +124,12 @@ describe("DeepSeek Harness configuration paths", () => { markerPath: root, }); expect( - clientSkillDirectories(home, ["dsh"], environment).directories, + clientSkillDirectories( + home, + ["dsh"], + environment, + HOST_PLATFORM.platform, + ).directories, ).toEqual([{ client: "dsh", directory: join(root, "skills") }]); }, ); @@ -136,9 +147,14 @@ describe("DeepSeek Harness configuration paths", () => { resolve("/custom/harness", "cordis.patch.yml"), ); expect( - clientSkillDirectories(join("/home/a"), ["dsh"], { - DSH_AGENTS_HOME: "/custom/agents", - }).directories, + clientSkillDirectories( + join("/home/a"), + ["dsh"], + { + DSH_AGENTS_HOME: "/custom/agents", + }, + HOST_PLATFORM.platform, + ).directories, ).toEqual([ { client: "dsh", directory: resolve("/custom/agents", "skills") }, ]); @@ -306,6 +322,58 @@ describe("DeepSeek Harness Cordis patch registration", () => { }); }); +describe("DeepSeek Harness caller-selected platform", () => { + it.each([ + ["win32", { GHIDRA_INSTALL_DIR: "/new", CALLER: "keep" }], + [ + "linux", + { + ghidra_install_dir: "/old", + CALLER: "keep", + GHIDRA_INSTALL_DIR: "/new", + }, + ], + ] as const)( + "writes environment names with %s identity", + async (platform, env) => { + const { client } = await harnessClient(); + await writeFile( + client.configPath, + `- insert: + - id: rea-mcp + name: "@deepseek-ai/dsh-mcp-client" + config: + serverName: rea + transport: stdio + command: stale-launcher + args: [mcp] + env: + ghidra_install_dir: /old + CALLER: keep +`, + ); + // Windows names match case-insensitively, so the caller's row is + // renamed in place; elsewhere the names are distinct settings. + expect( + ( + await configureClientConfiguration( + client, + { GHIDRA_INSTALL_DIR: "/new" }, + command, + undefined, + platform, + ) + ).status, + ).toBe("configured"); + const patches = parseYaml(await readFile(client.configPath, "utf8")) as { + insert: { config?: { env?: Record } }[]; + }[]; + const written = patches[0]?.insert[0]?.config?.env ?? {}; + expect(Object.entries(written)).toEqual(Object.entries(env)); + }, + ); +}); + describe("DeepSeek Harness registration identity", () => { it("applies a direct config patch before judging the registration", async () => { const { home, client } = await harnessClient(); diff --git a/tests/boundary/filesystem/hermesClientConfiguration.test.ts b/tests/boundary/filesystem/hermesClientConfiguration.test.ts index 7db8024f64..ba5693dd9e 100644 --- a/tests/boundary/filesystem/hermesClientConfiguration.test.ts +++ b/tests/boundary/filesystem/hermesClientConfiguration.test.ts @@ -313,7 +313,12 @@ describe("Hermes configuration paths", () => { expect(client.markerPath).toBe(selected); expect(client.configPath).toBe(`${selected}/config.yaml`); expect( - clientSkillDirectories(home, ["hermes"], environment).directories, + clientSkillDirectories( + home, + ["hermes"], + environment, + HOST_PLATFORM.platform, + ).directories, ).toEqual([{ client: "hermes", directory: `${selected}/skills` }]); expect( await configureClientConfiguration(client, {}, command), @@ -447,7 +452,12 @@ describe("Hermes unresolved home identity", () => { if (client === undefined) throw new Error("missing Hermes client"); expect(client.markerPath).toBe(selected); expect( - clientSkillDirectories(home, ["hermes"], environment).directories, + clientSkillDirectories( + home, + ["hermes"], + environment, + HOST_PLATFORM.platform, + ).directories, ).toEqual([{ client: "hermes", directory: join(selected, "skills") }]); expect( await configureClientConfiguration(client, {}, command), @@ -484,7 +494,12 @@ describe("Hermes unresolved home identity", () => { }); expect(client).not.toHaveProperty("markerPath"); expect( - clientSkillDirectories(home, ["hermes"], environment), + clientSkillDirectories( + home, + ["hermes"], + environment, + HOST_PLATFORM.platform, + ), ).toMatchObject({ directories: [], unresolved: [{ client: "hermes", path: selected }], diff --git a/tests/boundary/filesystem/kiloClientLocation.test.ts b/tests/boundary/filesystem/kiloClientLocation.test.ts index 016cb4acd5..10422a3214 100644 --- a/tests/boundary/filesystem/kiloClientLocation.test.ts +++ b/tests/boundary/filesystem/kiloClientLocation.test.ts @@ -196,7 +196,11 @@ it("refuses a relative Kilo configuration path instead of guessing its folder", // Readers such as doctor's provider settings refuse it instead of reading // a file relative to REA's own folder. expect( - await readEffectiveClientRegistrationConfiguration(resolved), + await readEffectiveClientRegistrationConfiguration( + resolved, + undefined, + HOST_PLATFORM.platform, + ), ).toMatchObject({ ok: false, error: { kind: "unreadable" } }); } }); diff --git a/tests/boundary/filesystem/kimiClientConfiguration.test.ts b/tests/boundary/filesystem/kimiClientConfiguration.test.ts index a849926940..e5f8fbbf15 100644 --- a/tests/boundary/filesystem/kimiClientConfiguration.test.ts +++ b/tests/boundary/filesystem/kimiClientConfiguration.test.ts @@ -72,9 +72,10 @@ describe("Kimi Code configuration paths and skills", () => { }); it("uses the existing shared skill destination without a separate skill root", () => { - expect(clientSkillDirectories("/home/a", ["kimi"]).directories).toEqual([ - { client: "shared", directory: "/home/a/.agents/skills" }, - ]); + expect( + clientSkillDirectories("/home/a", ["kimi"], {}, HOST_PLATFORM.platform) + .directories, + ).toEqual([{ client: "shared", directory: "/home/a/.agents/skills" }]); }); }); diff --git a/tests/boundary/filesystem/mcpAddClientLifecycle.test.ts b/tests/boundary/filesystem/mcpAddClientLifecycle.test.ts index cce3b6d474..6bb9588ed2 100644 --- a/tests/boundary/filesystem/mcpAddClientLifecycle.test.ts +++ b/tests/boundary/filesystem/mcpAddClientLifecycle.test.ts @@ -979,3 +979,29 @@ const gooseFixture = async (prefix: string) => { if (row === undefined) throw new Error("Goose has no table row"); return { home, team, row, environment, systemPaths }; }; + +it.skipIf(HOST_PLATFORM.platform === "win32")( + "secures a private profile on this host when setup targets another platform", + async () => { + const home = await createTestTempDirectory("rea-private-profile-host-"); + const row = supportedClients(home, HOST_PLATFORM.platform, { + HOME: home, + }).find(({ name }) => name === "fx"); + if (row?.privateProfile === undefined) + throw new Error("fx has no private profile row"); + await expect( + configureClientConfiguration(row, {}, command), + ).resolves.toMatchObject({ status: "configured" }); + const directory = dirname(row.configPath); + await chmod(row.configPath, 0o644); + await chmod(directory, 0o755); + + // The selected platform shapes the registration; the file's modes stay + // this host's, so a readable private profile is still repaired. + await expect( + configureClientConfiguration(row, {}, command, undefined, "win32"), + ).resolves.toMatchObject({ status: "configured" }); + expect((await stat(row.configPath)).mode & 0o777).toBe(0o600); + expect((await stat(directory)).mode & 0o777).toBe(0o700); + }, +); diff --git a/tests/boundary/filesystem/opencodeLayeredUninstall.test.ts b/tests/boundary/filesystem/opencodeLayeredUninstall.test.ts index 4edb1404b6..7355d7f34f 100644 --- a/tests/boundary/filesystem/opencodeLayeredUninstall.test.ts +++ b/tests/boundary/filesystem/opencodeLayeredUninstall.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; @@ -54,6 +55,8 @@ it.each(["native", "legacy"])( sourcePath, })), ), + undefined, + HOST_PLATFORM.platform, )?.servers.rea; const before = await effective(); expect(before).toMatchObject({ diff --git a/tests/boundary/filesystem/qwenClientConfiguration.test.ts b/tests/boundary/filesystem/qwenClientConfiguration.test.ts index efbe6b14e6..26ce30af9d 100644 --- a/tests/boundary/filesystem/qwenClientConfiguration.test.ts +++ b/tests/boundary/filesystem/qwenClientConfiguration.test.ts @@ -58,7 +58,9 @@ describe("Qwen Code configuration paths", () => { QWEN_HOME: override, }); expect( - supportedClients("/home/a").find(({ name }) => name === "qwen_code"), + supportedClients("/home/a", HOST_PLATFORM.platform).find( + ({ name }) => name === "qwen_code", + ), ).toEqual(client); expect(client).toMatchObject({ configPath: join(directory, "settings.json"), diff --git a/tests/boundary/filesystem/skillSelectionProvenance.test.ts b/tests/boundary/filesystem/skillSelectionProvenance.test.ts index 6f7881a2ab..f022838516 100644 --- a/tests/boundary/filesystem/skillSelectionProvenance.test.ts +++ b/tests/boundary/filesystem/skillSelectionProvenance.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js"; import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; @@ -27,8 +28,12 @@ it("uses the same Hermes root for sticky selection and named profile resolution" profile, ); expect( - clientSkillDirectories(home, ["hermes"], { HERMES_HOME: nested }) - .directories, + clientSkillDirectories( + home, + ["hermes"], + { HERMES_HOME: nested }, + HOST_PLATFORM.platform, + ).directories, ).toEqual([{ client: "hermes", directory: join(profile, "skills") }]); }); @@ -38,7 +43,12 @@ it("retains an explicitly selected missing Hermes profile as unresolved without await mkdir(root); await writeFile(join(root, "active_profile"), "missing-profile\n"); const environment = { HERMES_HOME: root }; - const selection = clientSkillDirectories(home, ["hermes"], environment); + const selection = clientSkillDirectories( + home, + ["hermes"], + environment, + HOST_PLATFORM.platform, + ); expect(selection).toMatchObject({ directories: [], unresolved: [ diff --git a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts index ba11b1cdf9..d005ccf469 100644 --- a/tests/boundary/filesystem/zcodeClientConfiguration.test.ts +++ b/tests/boundary/filesystem/zcodeClientConfiguration.test.ts @@ -1,3 +1,4 @@ +import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js"; import { currentClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js"; import { execFile } from "node:child_process"; import { mkdir, readFile, writeFile } from "node:fs/promises"; @@ -349,7 +350,9 @@ describe("ZCode fallback uninstall", () => { const native = JSON.stringify({ mcp: { servers: { rea: owned } } }); await writeFile(client.configPath, native); const fallback = await writeFallback(home, servers); - const cursor = supportedClients(home).find(({ name }) => name === "cursor"); + const cursor = supportedClients(home, HOST_PLATFORM.platform).find( + ({ name }) => name === "cursor", + ); if (cursor === undefined) throw new Error("missing Cursor client"); await mkdir(dirname(cursor.configPath), { recursive: true }); const otherClient = JSON.stringify({ mcpServers: { rea: owned } }); diff --git a/tests/process-global/updateMaintenance.test.ts b/tests/process-global/updateMaintenance.test.ts index 22f81d6bb7..f8e02d71b3 100644 --- a/tests/process-global/updateMaintenance.test.ts +++ b/tests/process-global/updateMaintenance.test.ts @@ -66,7 +66,9 @@ const plan = ( ); const writeClient = async (id: string, content: string): Promise => { - const client = supportedClients(home).find(({ name }) => name === id); + const client = supportedClients(home, HOST_PLATFORM.platform).find( + ({ name }) => name === id, + ); if (client === undefined) throw new Error(`Unknown test client ${id}`); await mkdir(dirname(client.configPath), { recursive: true }); await writeFile(client.configPath, content);