fix(hermes): preserve external YAML anchor consumers (#5152)

* fix(hermes): preserve consumers of anchored registrations

Keep original alias values and comments before setup or uninstall replaces an anchored REA node. Preserve original nested bindings and retained sharing through the common YAML writer.

Fixes #3325

* test(hermes): assert retained alias object identity

* fix(hermes): preserve removed keys and surviving alias owners

* test(cli): restore retained-view cancellation phase coverage

* fix(setup): retain copied Hermes anchor comments

* chore(hermes): reverse incidental merge hook changes

Reverse the merge relative to its included-main parent to restore exact upstream bytes. Reapply only the independently reviewed shared-owner and regression patch in a normal follow-up commit. Keep all ordinary history and normal hooks; no remote push of the rejected hook drift.

Refs #3325

* fix(hermes): retain reviewed shared-owner integration

* chore(hermes): reverse incidental merge hook changes

Reverse the merge relative to its included-main parent to restore exact upstream bytes. Reapply only the independently reviewed shared-owner and regression patch in a normal follow-up commit. Keep all ordinary history and normal hooks; no remote push of the rejected hook drift.

Refs #3325

* fix(hermes): retain exact reviewed shared-owner regression

* test(native): align plist command fixtures with main

Adopt the two maintained-main fixture files as of
dd198d037f. Distinguish exact XML lint
validation from conversion commands and retain truthful provenance,
source-value, format, limitations and unknown-scalar assertions.

The original seven provenance failures reproduce before this repair.
The affected three-file group passes all 86 checks afterward. Product
code and installed-package bytes remain unchanged.

Co-authored-by: morluto <[email protected]>

* fix(native): retain bound-target dispatch failure contracts

Adopt morluto's canonical Apple dispatch metadata owner and bounded MCP
and cleanup controls from 41e10b240c.
Keep fresh admission separate from already-bound target changes and
retain access-denied classification, exact physical causes and cleanup
ownership. Update the two stale unit expectations exposed by Source CI.

The original unit pair reproduces before the correction. Current focused
regressions, package verification and installed feature controls pass.
Only the canonical Apple owner and related controls are ported; this does
not integrate unrelated later-main changes or create another contribution.

Co-authored-by: morluto <[email protected]>

* test: align plist provenance and package listing fixtures

Keep the exact XML lint invocation in CLI provenance and include the
required CLI environment owner in the synthetic package member list.
Preserve binary provenance and all LF, CRLF, literal-CR and bytecode checks.

* revert(hermes): restore canonical main before reviewed replay

This reverts commit 575a10d194, reversing
changes made to bb17e90f27.

* test(cli): retain full cancellation oracle with prepared projection

---------

Co-authored-by: hhang <[email protected]>
Co-authored-by: gensokyo-dev <[email protected]>
Co-authored-by: morluto <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-27 21:33:17 +08:00
1 parent 095a51cf38
commit 84f62f76e5
7 files changed
+673 -49

No files matched your search

+6 -4
View File
@@ -497,6 +497,12 @@ comments and unrelated settings. It resolves that file from `HERMES_HOME`, else
from the platform default Hermes itself uses — `%LOCALAPPDATA%\hermes` on
Windows, `~/.hermes` elsewhere — including any `HERMES_DATA_DIR_SUFFIX`. Hermes
connects an entry unless `enabled` is false, so setup states `enabled: true`.
Aliases whose anchor is defined in the `rea` entry retain their original resolved
values when setup refreshes or uninstall removes that entry. Setup does not
redirect another server's alias to REA's new launcher or policy.
This also covers an anchor on the removed registration key. References to
definitions outside the entry keep sharing the original value, including when
another definition later reuses the same anchor name.
Hermes personal skills install under the active Hermes home's `skills` directory.
Setup, doctor and uninstall share REA's sticky-profile resolver.
@@ -734,10 +740,6 @@ can override an inherited disable. Lower files and other server entries remain
unchanged; a legacy server named `servers` that conflicts with the native table
requires manual repair before setup can write.
When Hermes aliases refer to an anchored REA entry, refreshing or removing REA
retains the aliases' original resolved values. Setup does not redirect another
server's alias to REA's new launcher or policy.
With no client selected, the interactive skill choice targets the shared
`~/.agents/skills` copy. Selecting a client targets its personal destination
when supported. Use `rea setup --skill-client shared --skill` for the shared
@@ -0,0 +1,192 @@
import {
Alias,
isAlias,
isMap,
isNode,
isSeq,
visit,
type Document,
type Node,
} from "yaml";
interface AliasCopies {
readonly targets: ReadonlyMap<Alias, Node | undefined>;
readonly removed: ReadonlySet<Node>;
readonly retained: Map<Node, Node>;
readonly survivingNames: Map<Node, string>;
readonly visiting: Set<Node>;
readonly validated: Set<Node>;
readonly names: Set<string>;
readonly reusedNames: ReadonlySet<string>;
}
const copyAliasComments = (source: Alias, copy: Node): Node => {
for (const key of ["commentBefore", "comment"] as const) {
const comment = source[key];
if (comment === undefined) continue;
copy[key] = copy[key] === undefined ? comment : `${copy[key]}\n${comment}`;
}
return copy;
};
const aliasTarget = (source: Alias, context: AliasCopies): Node => {
const target = context.targets.get(source);
if (target === undefined)
throw new TypeError("Cannot preserve an unresolved Hermes YAML alias");
return target;
};
/** Check copied dependencies even when their definitions will stay in place. */
const validateAliasValue = (source: Node, context: AliasCopies): void => {
if (context.validated.has(source)) return;
if (context.visiting.has(source))
throw new TypeError("Cannot preserve cyclic Hermes YAML aliases");
context.visiting.add(source);
visit(source, {
Alias: (_key, node) =>
validateAliasValue(aliasTarget(node, context), context),
});
context.visiting.delete(source);
context.validated.add(source);
};
const newAnchorName = (anchor: string, context: AliasCopies): string => {
let index = 1;
let name = `${anchor}_rea_preserved`;
while (context.names.has(name)) name = `${anchor}_rea_preserved_${index++}`;
context.names.add(name);
return name;
};
/** Reuse the surviving owner; a later same-name definition must not capture it. */
const survivingAlias = (target: Node, context: AliasCopies): Alias => {
if (isAlias(target) || target.anchor === undefined)
throw new TypeError("Invalid Hermes YAML alias target");
if (!context.reusedNames.has(target.anchor)) return new Alias(target.anchor);
let name = context.survivingNames.get(target);
if (name === undefined) {
name = newAnchorName(target.anchor, context);
context.survivingNames.set(target, name);
}
return new Alias(name);
};
/** Keep source bindings, including nested aliases whose names were later reused. */
const copyAliasValue = (
source: Node,
context: AliasCopies,
cloned?: Node,
): Node => {
if (isAlias(source)) {
const target = aliasTarget(source, context);
return copyAliasComments(
source,
context.removed.has(target)
? copyAliasValue(target, context)
: survivingAlias(target, context),
);
}
if (context.visiting.has(source))
throw new TypeError("Cannot preserve cyclic Hermes YAML aliases");
const retained = context.retained.get(source);
if (
retained !== undefined &&
!isAlias(retained) &&
retained.anchor !== undefined
)
return new Alias(retained.anchor);
const copy = cloned ?? source.clone();
if (!isNode(copy) || isAlias(copy))
throw new TypeError("Invalid Hermes YAML value copy");
if (source.anchor !== undefined) {
copy.anchor = newAnchorName(source.anchor, context);
context.retained.set(source, copy);
}
context.visiting.add(source);
const child = (value: unknown, copied: unknown): unknown =>
isNode(value)
? copyAliasValue(value, context, isNode(copied) ? copied : undefined)
: copied;
if (isMap(source) && isMap(copy)) {
for (const [index, pair] of source.items.entries()) {
const copied = copy.items[index];
if (copied === undefined) continue;
copied.key = child(pair.key, copied.key);
copied.value = child(pair.value, copied.value);
}
} else if (isSeq(source) && isSeq(copy)) {
copy.items = source.items.map((value, index) =>
child(value, copy.items[index]),
);
}
context.visiting.delete(source);
return copy;
};
/** Preserve other consumers before removing anchored configuration nodes. */
export const preserveHermesAliasValues = (
document: Document,
previous: readonly unknown[],
): void => {
const roots = previous.filter(isNode);
const removed = new Set<Node>();
for (const root of roots)
visit(root, {
Value: (_key, node) => {
if (node.anchor !== undefined) removed.add(node);
},
});
if (removed.size === 0) return;
const targets = new Map<Alias, Node | undefined>();
const names = new Set<string>();
const reusedNames = new Set<string>();
visit(document, {
Alias: (_key, node) => {
targets.set(node, node.resolve(document));
},
Value: (_key, node) => {
if (node.anchor === undefined) return;
if (names.has(node.anchor)) reusedNames.add(node.anchor);
names.add(node.anchor);
},
});
const context: AliasCopies = {
targets,
removed,
retained: new Map(),
survivingNames: new Map(),
visiting: new Set(),
validated: new Set(),
names,
reusedNames,
};
const replacements = new Map<Node, Node>();
visit(document, {
Alias: (_key, node, path) => {
if (!roots.some((root) => path.includes(root))) {
const target = targets.get(node);
if (target === undefined || !removed.has(target)) return;
validateAliasValue(target, context);
replacements.set(
node,
copyAliasComments(node, copyAliasValue(target, context)),
);
}
},
});
// Plan every binding against the original document before moving any anchor.
for (const [target, name] of context.survivingNames)
if (!isAlias(target)) target.anchor = name;
visit(document, {
Alias: (_key, node) => {
const replacement = replacements.get(node);
if (replacement !== undefined) return replacement;
const target = targets.get(node);
const name =
target === undefined ? undefined : context.survivingNames.get(target);
return name === undefined
? undefined
: copyAliasComments(node, new Alias(name));
},
});
};
@@ -7,6 +7,254 @@ import {
stringifyHermesConfigurationYaml,
} from "./HermesConfigurationYaml.js";
it("refreshes an anchored entry without changing another server's resolved value", () => {
const source =
"mcp_servers:\n rea: &server\n command: old\n enabled: false\n other: *server # Independent server\n";
const updated = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], {
command: "rea",
enabled: true,
});
expect(parseHermesConfigurationYaml(updated)).toEqual({
mcp_servers: {
rea: { command: "rea", enabled: true },
other: { command: "old", enabled: false },
},
});
expect(updated).toContain("# Independent server");
});
describe("Hermes surviving anchor definitions", () => {
for (const shadowed of [false, true]) {
it.each([undefined, { command: "rea" }])(
`retains surviving shared target identity with shadowed=${shadowed} when writing %j`,
(entry) => {
const source =
"defaults: &settings {MODE: 'off'}\nmcp_servers:\n rea: &server\n command: old\n env: *settings\n" +
(shadowed ? "later: &settings {MODE: 'later'}\n" : "") +
"observer: *server\npeer: *settings\n";
const result = parse(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], entry),
);
expect(result.defaults).toEqual({ MODE: "off" });
expect(result.observer.env).toBe(result.defaults);
expect(result.peer).toBe(shadowed ? result.later : result.defaults);
if (shadowed) expect(result.peer).not.toBe(result.defaults);
},
);
}
it.each(["settings", "missing"])(
"still refuses a surviving %s dependency that cannot be retained",
(reference) => {
const source = `defaults: &settings {dependency: *${reference}}\nmcp_servers:\n rea: &server {env: *settings}\nobserver: *server\n`;
expect(() =>
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], undefined),
).toThrow();
},
);
});
describe("Hermes copied anchor comments", () => {
it("retains the removed anchor definition comment at its surviving consumer", () => {
const source =
"mcp_servers:\n rea: &server {command: old} # Keep definition explanation\n other: *server\n";
const updated = editHermesConfigurationYaml(
source,
["mcp_servers", "rea"],
undefined,
);
expect(parseHermesConfigurationYaml(updated)).toEqual({
mcp_servers: { other: { command: "old" } },
});
expect(updated).toContain("# Keep definition explanation");
});
it.each([undefined, { command: "rea" }])(
"combines definition and consumer comments without copying shared definitions twice when writing %j",
(entry) => {
const source =
"mcp_servers:\n rea:\n # Definition leading note\n &server {command: old} # Definition trailing note\n other:\n # Consumer leading note\n *server # Consumer trailing note\nobserver: *server # Later consumer note\n";
const updated = editHermesConfigurationYaml(
source,
["mcp_servers", "rea"],
entry,
);
const result = parse(updated);
expect(result).toEqual({
mcp_servers: {
...(entry === undefined ? {} : { rea: entry }),
other: { command: "old" },
},
observer: { command: "old" },
});
expect(result.observer).toBe(result.mcp_servers.other);
for (const comment of [
"Definition leading note",
"Definition trailing note",
"Consumer leading note",
"Consumer trailing note",
"Later consumer note",
])
expect(updated.split(comment)).toHaveLength(2);
},
);
});
describe("Hermes anchored-entry consumers", () => {
it.each([undefined, { command: "rea" }])(
"preserves an anchored registration key when writing %j",
(entry) => {
const source =
"mcp_servers:\n &server_name rea: {command: old}\nobserver: *server_name # Keep registration name\n";
const updated = editHermesConfigurationYaml(
source,
["mcp_servers", "rea"],
entry,
);
expect(parseHermesConfigurationYaml(updated)).toEqual({
mcp_servers: entry === undefined ? {} : { rea: entry },
observer: "rea",
});
expect(updated).toContain("# Keep registration name");
if (entry !== undefined) {
expect(updated).toContain("&server_name rea:");
expect(updated).toContain("*server_name");
}
},
);
it("preserves both key and value consumers of a removed pair", () => {
const source =
"mcp_servers:\n &server_name rea: &server {command: old}\n other: *server\nobserver: *server_name\nlater: &server_name later\nlater_observer: *server_name\n";
expect(
parseHermesConfigurationYaml(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], undefined),
),
).toEqual({
mcp_servers: { other: { command: "old" } },
observer: "rea",
later: "later",
later_observer: "later",
});
});
it.each([undefined, { command: "rea" }])(
"preserves references to an anchored child when writing %j",
(entry) => {
const source =
"mcp_servers:\n rea:\n command: old\n env: &settings\n MODE: 'off'\n other:\n command: other\n env: *settings\n";
const result = parseHermesConfigurationYaml(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], entry),
);
expect(result).toEqual({
mcp_servers: {
...(entry === undefined ? {} : { rea: entry }),
other: { command: "other", env: { MODE: "off" } },
},
});
},
);
it.each([undefined, { command: "rea" }])(
"retains nested alias comments when writing %j",
(entry) => {
const source =
"defaults: &settings {MODE: 'old'}\nmcp_servers:\n rea: &entry\n command: old\n env:\n # Keep nested leading comment\n *settings # Keep nested reference comment\n other: *entry # Keep external reference comment\n";
const updated = editHermesConfigurationYaml(
source,
["mcp_servers", "rea"],
entry,
);
expect(parseHermesConfigurationYaml(updated)).toEqual({
defaults: { MODE: "old" },
mcp_servers: {
...(entry === undefined ? {} : { rea: entry }),
other: { command: "old", env: { MODE: "old" } },
},
});
expect(updated).toContain("# Keep nested leading comment");
expect(updated).toContain("# Keep nested reference comment");
expect(updated).toContain("# Keep external reference comment");
},
);
it("binds copied nested aliases before a later anchor reuses their name", () => {
const source =
"defaults: &settings {MODE: 'old'}\nmcp_servers:\n rea: &entry\n command: old\n env: *settings\nlater: &settings {MODE: 'later'}\nobserver: *entry\n";
const result = parseHermesConfigurationYaml(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], {
command: "rea",
}),
);
expect(result).toEqual({
defaults: { MODE: "old" },
mcp_servers: { rea: { command: "rea" } },
later: { MODE: "later" },
observer: { command: "old", env: { MODE: "old" } },
});
});
it("changes only consumers of the removed definition when an anchor name repeats", () => {
const source =
"mcp_servers:\n rea: &server {command: old}\n other: *server\nlater: &server {command: later}\nobserver: *server\n";
expect(
parseHermesConfigurationYaml(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], undefined),
),
).toEqual({
mcp_servers: { other: { command: "old" } },
later: { command: "later" },
observer: { command: "later" },
});
});
it.each([true, false])(
"keeps shared child identity with child first=%s",
(childFirst) => {
const child = "child: *settings\n";
const parent = "parent: *entry\n";
const source =
"mcp_servers:\n rea: &entry\n command: old\n env: &settings {MODE: 'off'}\n" +
(childFirst ? child + parent : parent + child);
const result = parse(
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], undefined),
);
expect(result).toEqual({
mcp_servers: {},
child: { MODE: "off" },
parent: { command: "old", env: { MODE: "off" } },
});
expect(result.child).toBe(result.parent.env);
},
);
it("retains one source copy for repeated consumers instead of expanding every alias", () => {
const source =
"mcp_servers:\n rea: &server {command: old}\nobservers:\n" +
" - *server\n".repeat(50);
const updated = editHermesConfigurationYaml(
source,
["mcp_servers", "rea"],
undefined,
);
expect(parseHermesConfigurationYaml(updated)).toEqual({
mcp_servers: {},
observers: Array.from({ length: 50 }, () => ({ command: "old" })),
});
expect(updated.match(/command: old/gu)).toHaveLength(1);
});
it.each(["server", "missing"])(
"still refuses a copied %s dependency that cannot be retained",
(reference) => {
const source = `mcp_servers:\n rea: &server\n command: old\n dependency: *${reference}\nobserver: *server\n`;
expect(() =>
editHermesConfigurationYaml(source, ["mcp_servers", "rea"], undefined),
).toThrow();
},
);
});
for (const version of ["1.1", "1.2"] as const)
it.each(["refresh", "remove"] as const)(
`preserves YAML ${version} consumers of a replaced REA anchor during %s`,
+11 -30
View File
@@ -7,12 +7,12 @@ import {
parseDocument,
Document,
Schema,
visit,
type ParsedNode,
type Node,
type ScalarTag,
type Tags,
} from "yaml";
import { preserveHermesAliasValues } from "./HermesConfigurationAliases.js";
const SOURCE_SCALAR_TAGS = new Set([
"tag:yaml.org,2002:int",
@@ -395,33 +395,6 @@ const copyHermesAliasSource = (
return copied;
};
/** External aliases keep their original bindings when their source is replaced. */
const retainHermesAliasConsumers = (
document: Document,
previous: unknown,
): void => {
if (!isNode(previous)) return;
const replaced = new Set<Node>();
visit(previous, {
Node: (_key, node) => {
replaced.add(node);
},
});
visit(document, {
Alias: (_key, alias, path) => {
const source = alias.resolve(document);
if (
source === undefined ||
!replaced.has(source) ||
path.some((parent) => isNode(parent) && replaced.has(parent))
)
return;
const copied = copyHermesAliasSource(alias, document);
return isNode(copied) ? copied : undefined;
},
});
};
/** Set or delete one value in a Hermes `config.yaml`, keeping the rest as written. */
export const editHermesConfigurationYaml = (
text: string,
@@ -453,11 +426,19 @@ export const editHermesConfigurationYaml = (
document.setIn(parent, document.createNode({}));
}
}
retainHermesAliasConsumers(document, document.getIn(path, true));
const previous = document.getIn(path, true);
// Deletion removes the pair's key too; replacement retains that key node.
const parent =
value === undefined ? document.getIn(path.slice(0, -1), true) : undefined;
const removedKey = isMap(parent)
? parent.items.find(
(pair) => isScalar(pair.key) && pair.key.value === path.at(-1),
)?.key
: undefined;
preserveHermesAliasValues(document, [previous, removedKey]);
if (value === undefined) {
if (!document.deleteIn(path)) return text;
} else {
const previous = document.getIn(path, true);
// Let YAML's scalar update preserve numeric formats for a direct edit.
// Replacing containers needs explicit retention of unchanged child nodes.
document.setIn(
@@ -0,0 +1,156 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { parse } from "yaml";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
for (const version of ["1.1", "1.2"] as const) {
for (const style of [
"entry",
"child",
"shadowed",
"key",
"external",
] as const) {
cliTest(
`Hermes ${version} preserves ${style} anchor consumers through setup and uninstall`,
async ({ cli }) => {
const home = await createTestTempDirectory(
"rea-hermes-anchored-entry-",
);
const profile = join(home, "hermes");
const config = join(profile, "config.yaml");
const server = {
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
env: { KEEP: "off" },
enabled: false,
};
const directive = version === "1.2" ? "%YAML 1.2\n---\n" : "";
const hasDefaults = style === "shadowed" || style === "external";
const entry =
` command: ${JSON.stringify(server.command)}\n` +
` args: ${JSON.stringify(server.args)}\n` +
" enabled: false\n" +
(hasDefaults
? " env: *settings # Keep nested reference comment\n"
: ` env: ${style === "child" ? "&settings " : ""}{KEEP: 'off'}\n`);
const original =
directive +
"# Keep this user setting\nmodel: custom\n" +
(hasDefaults ? "defaults: &settings {KEEP: 'off'}\n" : "") +
`mcp_servers:\n ${style === "key" ? "&server_name " : ""}rea:${style === "child" || style === "key" ? "" : " &server"}${style === "entry" ? " # Keep anchor definition comment" : ""}\n` +
entry +
(style === "child"
? " other:\n command: other-server\n env: *settings # Shared environment\n"
: style === "key"
? " other: {command: other-server}\nobserver: *server_name # Registration name\n"
: " other: *server # Independent server\n") +
(style === "shadowed"
? "later: &settings {KEEP: later}\nobserver: *server\n"
: "");
const other =
style === "child"
? { command: "other-server", env: { KEEP: "off" } }
: style === "key"
? { command: "other-server" }
: server;
await mkdir(profile);
await writeFile(config, original);
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: arguments_,
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
HERMES_HOME: profile,
},
timeoutMs: 20_000,
});
const setup = [
"setup",
"--client",
"hermes",
"--skill=false",
"--yes",
"--json",
];
const applied = await run(setup);
expect(applied.exitCode, JSON.stringify(applied)).toBe(0);
expect(applied.json).toMatchObject({ status: "ready" });
const configured = await readFile(config, "utf8");
const configuredValues = parse(configured, { version });
expect(configuredValues).toMatchObject({
model: "custom",
mcp_servers: { rea: { enabled: true, env: { KEEP: "off" } }, other },
...(style === "shadowed"
? { observer: server, later: { KEEP: "later" } }
: {}),
...(style === "key" ? { observer: "rea" } : {}),
});
if (hasDefaults)
expect(configuredValues.mcp_servers.other.env).toBe(
configuredValues.defaults,
);
expect(configured).toContain("# Keep this user setting");
if (style === "entry")
expect(configured).toContain("# Keep anchor definition comment");
expect(configured).toContain(
style === "child"
? "# Shared environment"
: style === "key"
? "# Registration name"
: "# Independent server",
);
if (hasDefaults)
expect(configured).toContain("# Keep nested reference comment");
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const doctor = await run(["doctor", "--client", "hermes", "--json"]);
expect(doctor.exitCode, JSON.stringify(doctor)).toBe(0);
expect(doctor.json).toMatchObject({
healthy: true,
identity: { registrations: [{ client: "hermes", state: "aligned" }] },
});
const repeated = await run(setup);
expect(repeated.exitCode, JSON.stringify(repeated)).toBe(0);
expect(repeated.json).toMatchObject({
plannedActions: [],
appliedActions: [],
});
expect(await readFile(config, "utf8")).toBe(configured);
const removed = await run(["uninstall", "--json"]);
expect(removed.exitCode, JSON.stringify(removed)).toBe(0);
const removedValues = parse(await readFile(config, "utf8"), {
version,
});
expect(removedValues).toEqual({
model: "custom",
mcp_servers: { other },
...(hasDefaults ? { defaults: { KEEP: "off" } } : {}),
...(style === "shadowed"
? {
later: { KEEP: "later" },
observer: server,
}
: {}),
...(style === "key" ? { observer: "rea" } : {}),
});
if (hasDefaults)
expect(removedValues.mcp_servers.other.env).toBe(
removedValues.defaults,
);
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const remaining = await readFile(config, "utf8");
if (style === "entry")
expect(remaining).toContain("# Keep anchor definition comment");
if (hasDefaults)
expect(remaining).toContain("# Keep nested reference comment");
expect((await run(["uninstall", "--json"])).exitCode).toBe(0);
expect(await readFile(config, "utf8")).toBe(remaining);
},
);
}
}
@@ -289,6 +289,7 @@ describe.skipIf(process.platform !== "darwin")(
{
tool: "plutil",
command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"],
exit: { code: 0, signal: null },
},
]
: [{ tool: "file" }],
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { Cli } from "incur";
import { afterEach, expect, it, vi } from "vitest";
import * as analysisViewService from "../../src/application/analysisView/AnalysisViewService.js";
import * as evidenceInputResolver from "../../src/application/EvidenceInputResolver.js";
import { WasmArtifactService } from "../../src/application/wasm/WasmArtifactService.js";
import { registerAnalysisViewCommands } from "../../src/cli/analysisViewCommands.js";
@@ -331,6 +332,43 @@ it.each([
},
);
const referenceViewEvidence = async (
request: Parameters<typeof analysisViewService.inspectAnalysisView>[0],
evidenceBundlePath: string,
phase: string,
) => {
if (phase !== "projection") return undefined;
const result = await analysisViewService.inspectAnalysisView(
request,
undefined,
{ evidenceBundlePath },
);
if (!result.ok) throw new Error("Expected completed reference Evidence");
return result.value;
};
type PreparedView = ReturnType<
typeof analysisView.prepareAnalysisViewProjection
>;
const expectRetainedProjection = (
observation: unknown,
reference: Awaited<ReturnType<typeof referenceViewEvidence>>,
projected: ReturnType<PreparedView["project"]> | undefined,
parent: ReturnType<typeof analysisViewLayoutEvidence>,
) => {
if (!projected?.ok || reference === undefined)
throw new Error("Expected a completed page and reference Evidence");
const partial = parseEvidence(observation);
expect(partial).toEqual(reference);
expect(partial.normalized_result).toEqual(projected.value);
expect(partial).toMatchObject({
operation: "inspect_analysis_view",
evidence_links: [parent.evidence_id],
subject: parent.subject,
});
};
it.each([
["bundle", "control", "close"],
["bundle", "SIGINT", "close"],
@@ -399,13 +437,17 @@ it.each([
});
});
};
const expectedProjection = await referenceViewEvidence(
request,
path,
phase,
);
// Intercept the prepared owner, whose project method keeps its admission
// budget and calls the domain projector lexically rather than by export.
const originalPrepare = analysisView.prepareAnalysisViewProjection;
type Projection = ReturnType<typeof originalPrepare>;
let projected: ReturnType<Projection["project"]> | undefined;
let projected: ReturnType<PreparedView["project"]> | undefined;
const project = vi
.fn<(parent: Parameters<Projection["project"]>[0]) => void>()
.fn<(parent: Parameters<PreparedView["project"]>[0]) => void>()
.mockName("preparedProjection.project");
vi.spyOn(analysisView, "prepareAnalysisViewProjection").mockImplementation(
(...args) => {
@@ -421,13 +463,13 @@ it.each([
},
);
const originalResolve = evidenceInputResolver.resolveEvidenceInput;
vi.spyOn(evidenceInputResolver, "resolveEvidenceInput").mockImplementation(
(...args) => {
const resolve = vi
.spyOn(evidenceInputResolver, "resolveEvidenceInput")
.mockImplementation((...args) => {
const resolved = originalResolve(...args);
if (phase === "source-resolution-failure") queueInterrupt();
return resolved;
},
);
});
const originalClose = OwnedFileHandle.prototype.close;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
@@ -477,15 +519,17 @@ it.each([
expect(project).toHaveBeenCalledTimes(
Number(signal === "control" || phase === "projection"),
);
if (phase.startsWith("authentication"))
expect(resolve).not.toHaveBeenCalled();
if (phase === "projection" || phase === "source-resolution-failure")
expect(resolve).toHaveBeenCalledOnce();
if (phase === "projection") {
if (!projected?.ok) throw new Error("Expected a completed page");
const partial = parseEvidence(result.details.partial_observation);
expect(partial.normalized_result).toEqual(projected.value);
expect(partial).toMatchObject({
operation: "inspect_analysis_view",
evidence_links: [parent.evidence_id],
subject: parent.subject,
});
expectRetainedProjection(
result.details.partial_observation,
expectedProjection,
projected,
parent,
);
}
} finally {
await pendingInterrupt;