fix(setup): report the effective ZCode fallback registration

Resolve native-versus-fallback precedence once at the parsed configuration owner. Status and maintenance consume the selected dialect and path, including invalid and disabled fallback registrations.

Validation: 28 ZCode boundary and 26 maintenance tests pass. No configuration writes introduced.

Based on #3123.

Co-authored-by: IvenKooLab <276572622@qq.com>
This commit is contained in:
morluto
2026-10-14 13:58:15 +08:00
parent 03ffb2c13d
commit 583166785f
4 changed files with 202 additions and 19 deletions
@@ -172,3 +172,47 @@ export const effectiveClientConfiguration = (
}
return { ...last, servers, legacyServers: {} };
};
/** Read the configuration ZCode will load, preserving its same-scope fallback rule. */
export const readEffectiveClientRegistrationConfiguration = async (
client: SetupClient,
resources?: ArtifactResourceScope,
): Promise<
Result<
{
readonly document: ClientConfigurationDocument | undefined;
readonly configPath: string;
},
ClientConfigurationFileError
>
> => {
const nativeSources = await readClientConfigurationFiles(client, resources);
if (!nativeSources.ok) return nativeSources;
const nativeDocument = effectiveClientConfiguration(nativeSources.value);
const fallbackPath =
client.format === "zcode" ? client.zcodeFallbackPath : undefined;
if (
fallbackPath === undefined ||
(nativeDocument !== undefined &&
Object.keys(nativeDocument.servers).length > 0)
)
return ok({ document: nativeDocument, configPath: client.configPath });
const fallbackClient: SetupClient = {
...client,
configPath: fallbackPath,
configPaths: [fallbackPath],
format: "zcode_fallback",
};
const fallbackSources = await readClientConfigurationFiles(
fallbackClient,
resources,
);
if (!fallbackSources.ok) return fallbackSources;
const fallbackDocument = effectiveClientConfiguration(fallbackSources.value);
return ok(
fallbackDocument === undefined
? { document: nativeDocument, configPath: client.configPath }
: { document: fallbackDocument, configPath: fallbackPath },
);
};
+33 -19
View File
@@ -1,8 +1,5 @@
import { readClientPolicyBlock } from "./GeminiClientSettings.js";
import {
effectiveClientConfiguration,
readClientConfigurationFiles,
} from "./ClientConfigurationFiles.js";
import { readEffectiveClientRegistrationConfiguration } from "./ClientConfigurationFiles.js";
import {
clientServerForcedEnabled,
clientServerListedDisabled,
@@ -144,24 +141,26 @@ export const readClientRegistrationInspections = async (
});
continue;
}
let selectedConfigPath = client.configPath;
try {
const files = await readClientConfigurationFiles(
const configuration = await readEffectiveClientRegistrationConfiguration(
client,
options.resources,
);
if (!files.ok) {
if (!configuration.ok) {
record({
client: client.name,
config_path: client.configPath,
config_path: configuration.error.path,
command: [],
state: "invalid",
remediation: files.error.detail,
remediation: configuration.error.detail,
});
continue;
}
const parsed = effectiveClientConfiguration(files.value);
const { document: parsed, configPath } = configuration.value;
selectedConfigPath = configPath;
if (parsed === undefined) {
record(unavailableStatus(client, "missing"));
record(unavailableStatus(client, "missing", configPath));
continue;
}
const policy = await readClientPolicyBlock(
@@ -173,7 +172,7 @@ export const readClientRegistrationInspections = async (
if (!policy.ok) {
record({
client: client.name,
config_path: client.configPath,
config_path: configPath,
command: [],
state: "invalid",
remediation: policy.error.detail,
@@ -190,17 +189,26 @@ export const readClientRegistrationInspections = async (
if (editabilityError !== undefined) {
record({
client: client.name,
config_path: client.configPath,
config_path: configPath,
command: [],
state: "invalid",
remediation: editabilityError.message,
});
continue;
}
record(unavailableStatus(client, "missing"));
record(unavailableStatus(client, "missing", configPath));
continue;
}
const registration = parseRegistration(raw, client);
const effectiveClient: SetupClient =
parsed.dialect === "zcode_fallback"
? {
...client,
configPath,
configPaths: [configPath],
format: "zcode_fallback",
}
: client;
const registration = parseRegistration(raw, effectiveClient);
const command: RegistrationCommand = [
registration.command,
...registration.args,
@@ -213,13 +221,13 @@ export const readClientRegistrationInspections = async (
packageVersion !== PRODUCT_IDENTITY.packageVersion;
record(
configuredStatus(
client,
effectiveClient,
command,
activation === "enabled" &&
!packageVersionDrift &&
registrationAligned(
registration,
client,
effectiveClient,
currentCommandPath,
options.platform ?? process.platform,
) &&
@@ -237,7 +245,11 @@ export const readClientRegistrationInspections = async (
);
} catch (cause: unknown) {
record(
unavailableStatus(client, isMissing(cause) ? "missing" : "invalid"),
unavailableStatus(
client,
isMissing(cause) ? "missing" : "invalid",
selectedConfigPath,
),
);
}
}
@@ -254,7 +266,8 @@ const registrationActivation = (
(client.format !== "grok" && registration.disabled === true) ||
(registration.enabled === false &&
!clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey)) ||
(client.format === "zcode" && registration.enable === false) ||
((parsed.dialect === "zcode" || parsed.dialect === "zcode_fallback") &&
registration.enable === false) ||
clientServerListedDisabled(parsed, PRODUCT_IDENTITY.mcpServerKey)
? "disabled"
: "enabled";
@@ -435,9 +448,10 @@ const packageVersionRemediation = (
const unavailableStatus = (
client: SetupClient,
state: "missing" | "invalid",
configPath: string = client.configPath,
): ClientRegistrationStatus => ({
client: client.name,
config_path: client.configPath,
config_path: configPath,
command: [],
state,
remediation: remediation(client),
@@ -457,6 +457,115 @@ describe("ZCode disabled registrations", () => {
).toBe("configured");
expect(await statusFor()).toMatchObject({ state: "aligned", command });
});
it("reports the effective same-scope fallback registration and source path", async () => {
const { home, client } = await zcodeClient();
const fallback = client.zcodeFallbackPath;
if (fallback === undefined) throw new Error("missing ZCode fallback path");
await mkdir(join(home, ".agents"), { recursive: true });
await writeFile(
fallback,
JSON.stringify({
mcpServers: {
rea: { command: command[0], args: command.slice(1) },
},
}),
);
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "zcode"),
).toMatchObject({
config_path: fallback,
command,
state: "aligned",
});
});
it("reports a selected fallback parse failure as invalid at its source path", async () => {
const { home, client } = await zcodeClient();
const fallback = client.zcodeFallbackPath;
if (fallback === undefined) throw new Error("missing ZCode fallback path");
await mkdir(join(home, ".agents"), { recursive: true });
await writeFile(fallback, "{ malformed");
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "zcode"),
).toMatchObject({ config_path: fallback, state: "invalid" });
});
it("reports disabled fallback registrations as stale at the fallback path", async () => {
const { home, client } = await zcodeClient();
const fallback = client.zcodeFallbackPath;
if (fallback === undefined) throw new Error("missing ZCode fallback path");
await mkdir(join(home, ".agents"), { recursive: true });
await writeFile(
fallback,
JSON.stringify({
mcpServers: {
rea: { command: command[0], args: command.slice(1), enable: false },
},
}),
);
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "zcode"),
).toMatchObject({
config_path: fallback,
command,
state: "stale",
});
});
it("prefers a nonempty native server table over the fallback", async () => {
const { home, client } = await zcodeClient();
const fallback = client.zcodeFallbackPath;
if (fallback === undefined) throw new Error("missing ZCode fallback path");
await mkdir(join(home, ".agents"), { recursive: true });
await writeFile(
fallback,
JSON.stringify({
mcpServers: {
rea: { command: "fallback-command", args: ["mcp"] },
},
}),
);
await writeFile(
client.configPath,
JSON.stringify({
mcp: {
servers: { rea: { command: command[0], args: command.slice(1) } },
},
}),
);
expect(
(
await readClientRegistrationStatuses(home, undefined, {
platform: process.platform,
environment: {},
})
).find(({ client }) => client === "zcode"),
).toMatchObject({
config_path: client.configPath,
command,
state: "aligned",
});
});
});
describe("ZCode strict native JSON", () => {
@@ -146,6 +146,22 @@ describe("maintenance activation and layered configuration", () => {
expect(await readFile(configPath, "utf8")).toBe(original);
});
it("selects a ZCode registration loaded from the same-scope fallback", async () => {
await writeClient("zcode", JSON.stringify({ mcp: { servers: {} } }));
const fallbackPath = join(home, ".agents", "mcp.json");
await mkdir(join(home, ".agents"), { recursive: true });
await writeFile(
fallbackPath,
JSON.stringify({
mcpServers: { rea: { command: entryPoint, args: ["mcp"] } },
}),
);
await expect(
existingMaintenanceScope(home, entryPoint, process.env),
).resolves.toMatchObject({ clients: ["zcode"] });
});
it("selects an enabled registration inherited through layered OpenCode files", async () => {
const directory = join(home, ".config", "opencode");
await mkdir(directory, { recursive: true });