fix(ci): reconcile lifecycle checks with current main

Preserve failed event finalization across retries and current capture ownership checks. Retain Git unreadable-ref and bounded-heap Evidence coverage, pinned maintenance destinations, and explicit native binding facts.
This commit is contained in:
N0zoM1z0 committed 2026-10-11 01:08:09 +08:00
commit cace19caad
12 files changed
+133 -85

No files matched your search

@@ -2,7 +2,6 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
@@ -32,15 +31,16 @@ const graphIdentities = async (source: string) => {
...(identity.strategy === "artifact-local-key"
? {
namespace: identity.namespace,
// Module-export identity is a JSON tuple whose first entry
// binds the whole container; only that digest changes here.
key:
identity.namespace === "module-export"
? JSON.stringify(
z
.array(z.string())
.parse(JSON.parse(identity.key))
.slice(1),
(JSON.parse(identity.key) as unknown[]).slice(1),
)
: identity.key.replace(/^[0-9a-f]{64}:/u, ""),
: identity.namespace === "source-module"
? identity.key.replace(/^[0-9a-f]{64}:/u, "")
: identity.key,
}
: {}),
}))
+2
View File
@@ -69,6 +69,8 @@ export const closeCdpTargetSession = async (
}
}
}
// Cancellation skips optional domain shutdown but still releases the
// attached session using its independent, bounded cleanup signal.
if (!cleanup.signal.aborted && sessionId !== undefined)
try {
await connection.send(
@@ -553,7 +553,7 @@ it("shares concurrent close and retries browser cleanup after failure", async ()
expect(cleanupSettlements).toEqual([false, true]);
});
it("reports event finalization failure before allowing settled cleanup retries", async () => {
it("keeps failed event finalization failed across cleanup retries", async () => {
const finishEvents = vi
.fn<() => Promise<void>>()
.mockRejectedValue(new Error("event finalization failed"));
@@ -567,9 +567,10 @@ it("reports event finalization failure before allowing settled cleanup retries",
await expect(cleanup.close(finishEvents)).rejects.toMatchObject({
reason: "cleanup_failed",
cleanup: { reason: "event finalization failed" },
});
await expect(cleanup.close(finishEvents)).resolves.toBeUndefined();
await expect(cleanup.close(finishEvents)).rejects.toMatchObject({
reason: "cleanup_failed",
});
expect(finishEvents).toHaveBeenCalledTimes(1);
expect(closeBrowser).toHaveBeenCalledTimes(1);
});
+4 -5
View File
@@ -42,7 +42,6 @@ export class PlaywrightScenarioBrowserCleanupOwner {
#profileRemoved: boolean;
#closePromise: Promise<void> | undefined;
#eventFinalizationPromise: Promise<void> | undefined;
#eventFinalizationFailureReported = false;
/** Keep the narrow provider actions needed to release one opened browser. */
constructor(private readonly resources: PlaywrightScenarioCleanupResources) {
@@ -98,10 +97,10 @@ export class PlaywrightScenarioBrowserCleanupOwner {
try {
await eventFinalization;
} catch (cause: unknown) {
if (!this.#eventFinalizationFailureReported) {
failures.push(cause);
this.#eventFinalizationFailureReported = true;
}
// A failed finalization stays failed: the memoized attempt rejects on
// every retry so cleanup can never report success for unfinalized
// events.
failures.push(cause);
}
if (!this.#browserClosed) {
try {
@@ -22,9 +22,13 @@ it("preserves a 12,000-member native import through both analyzers", () => {
const staticAnalysis = analyzeParsedJavaScriptStaticSource("", parsed);
const ir = analyzeParsedJavaScriptSemantics(parsed);
expect(staticAnalysis.parse_status).toBe("complete");
// The addon exports `next`; `last` belongs to an object reached from it.
expect(staticAnalysis.electron.native_addon_bindings).toEqual([
expect.objectContaining({ specifier: "./addon.node", members: ["next"] }),
expect.objectContaining({
specifier: "./addon.node",
members: ["next"],
namespace_access: false,
dynamic_member_access: false,
}),
]);
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
const path = origin(onlyBinding(ir, "value"));
+21 -15
View File
@@ -16,35 +16,41 @@ import { classifyFilesystemEffects } from "./ProcessFilesystemEffects.js";
it.each(["after", "before"] as const)(
"keeps absence unknown when the %s snapshot omits an existing file",
async (partialSide) => {
// Enumeration order is filesystem-dependent. Choose a genuinely omitted
// entry by comparing a complete snapshot with a capacity-limited one.
const root = await createTestTempDirectory("rea-fs-partial-absence-");
const names = ["a.txt", "z.txt"];
for (const name of names) await writeFile(join(root, name), "unchanged");
await writeFile(join(root, "a1.txt"), "first");
await writeFile(join(root, "a2.txt"), "second");
await writeFile(join(root, "z.txt"), "target");
const scenario = parseProcessScenario({
executable: process.execPath,
working_directory: root,
filesystem_observation_paths: [root],
limits: { files: 2 },
});
const complete = await snapshotRoots({
...scenario,
limits: { ...scenario.limits, files: 4 },
});
const partial = await snapshotRoots(scenario);
const omitted = names.find(
(name) => !partial.files.some(({ path }) => path === `root_0:${name}`),
expect(complete.completeRoots).toEqual(["root_0"]);
expect(partial.truncated).toBe(true);
const omitted = complete.files.find(
(file) => !partial.files.some(({ path }) => path === file.path),
);
if (omitted === undefined)
throw new Error("Expected an omitted fixture file");
for (const name of names) if (name !== omitted) await rm(join(root, name));
const complete = await snapshotRoots(scenario);
if (omitted === undefined) throw new Error("Expected an omitted entry");
const before = partialSide === "before" ? partial : complete;
const after = partialSide === "after" ? partial : complete;
expect(await readFile(join(root, omitted), "utf8")).toBe("unchanged");
const effect = classifyFilesystemEffects(before, after).find(
({ path }) => path === `root_0:${omitted}`,
({ path }) => path === omitted.path,
);
expect(effect?.status).toBe("unknown");
expect(effect).toMatchObject(
partialSide === "after"
? { before: { path: `root_0:${omitted}` }, after: null }
: { before: null, after: { path: `root_0:${omitted}` } },
);
expect(effect).toMatchObject({
...(partialSide === "after"
? { before: omitted, after: null }
: { before: null, after: omitted }),
reason: expect.stringContaining("did not exhaust path enumeration"),
});
},
);
+18 -5
View File
@@ -18,6 +18,7 @@ import {
} from "./ProcessCaptureError.js";
import {
releaseProcessResources,
ProcessCaptureResourceScope,
createProcessCaptureObservationBuffer,
prepareProcessCapture,
resolveProcessResult,
@@ -291,26 +292,31 @@ it("keeps empty cleanup exception messages actionable in the report", async () =
});
it.each(["termination", "verification"] as const)(
"retains observations and the temporary root when process %s throws",
"retains observations and retries the temporary root when process %s throws",
async (stage) => {
const cleanupFailure = new Error(`${stage} inspection failed`);
const removeTemporaryRoot = vi.fn(async () => undefined);
let failing = true;
const scope = new ProcessCaptureResourceScope();
const host: ProcessCaptureCleanupHost = {
platform: "linux",
cleanupProcessGroup: async () => {
if (stage === "termination") throw cleanupFailure;
if (failing && stage === "termination") throw cleanupFailure;
return {
cleaned: true,
signaled: true,
unverified: [{ pid: 900, diagnostic: "environment unavailable" }],
unverified: failing
? [{ pid: 900, diagnostic: "environment unavailable" }]
: [],
};
},
verifyTokenOwnedProcesses: async () => {
throw cleanupFailure;
if (failing) throw cleanupFailure;
return { cleaned: true, signaled: false };
},
removeTemporaryRoot,
};
const cleanup = await releaseProcessResources({
const cleanup = await scope.release({
timers: new Set(),
terminal: { pid: 321 },
renderer: undefined,
@@ -360,6 +366,13 @@ it.each(["termination", "verification"] as const)(
},
});
}
failing = false;
await scope.retryCleanup();
expect(removeTemporaryRoot).toHaveBeenCalledExactlyOnceWith(
"/fixture/root",
);
await scope.close();
expect(removeTemporaryRoot).toHaveBeenCalledTimes(1);
},
);
@@ -17,6 +17,7 @@ import { parse as parseJsonc, type ParseError } from "jsonc-parser";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { clearClientLocationEnvironment } from "../../fixtures/clientEnvironment.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js";
import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js";
@@ -31,8 +32,7 @@ import {
} from "../../../src/application/Uninstall.js";
const roots: string[] = [];
const managedSkillManifest =
"---\nname: reverse-engineer-anything\n---\n# REA\n";
const ownedSkillManifest = `---\nname: ${PRODUCT_IDENTITY.skillName}\n---\n# REA\n`;
beforeEach(clearClientLocationEnvironment);
afterEach(async () => {
vi.unstubAllEnvs();
@@ -1355,7 +1355,7 @@ describe("client configuration filesystem removal", () => {
},
}),
);
await writeFile(skill, managedSkillManifest);
await writeFile(skill, ownedSkillManifest);
await mkdir(join(home, ".rea"), { recursive: true });
await import("node:fs/promises").then(({ symlink }) =>
symlink(home, join(home, ".rea/cache")),
@@ -1424,7 +1424,9 @@ describe("client configuration filesystem removal", () => {
readFile(`${configPath}.rea.backup`, "utf8"),
).rejects.toThrow();
});
});
describe("uninstall configuration preflight", () => {
it("fails closed on malformed client configuration", async () => {
const home = await createTestTempDirectory("rea-uninstall-bad-");
roots.push(home);
@@ -1443,16 +1445,18 @@ describe("client configuration filesystem removal", () => {
);
expect(await readFile(config, "utf8")).toBe("not-json");
});
});
describe("client configuration uninstall preflight", () => {
it("stops before any removal when one client configuration is malformed", async () => {
const home = await createTestTempDirectory("rea-uninstall-stop-");
roots.push(home);
const codex = join(home, ".codex/config.toml");
const cursor = join(home, ".cursor/mcp.json");
const skillMarker = join(
home,
".agents/skills/reverse-engineer-anything/marker.txt",
);
const managed = [
join(home, ".agents/skills/reverse-engineer-anything/SKILL.md"),
skillMarker,
join(home, ".rea/cache/marker.txt"),
join(home, ".rea/state/marker.txt"),
];
@@ -1466,11 +1470,9 @@ describe("client configuration uninstall preflight", () => {
await mkdir(dirname(path), { recursive: true });
await writeFile(codex, "[mcp_servers.rea]\ncommand = [broken\n");
await writeFile(cursor, cursorConfig);
for (const path of managed)
await writeFile(
path,
path === managed[0] ? managedSkillManifest : "marker",
);
for (const path of managed) await writeFile(path, "marker");
const manifest = join(dirname(skillMarker), "SKILL.md");
await writeFile(manifest, ownedSkillManifest);
const stopped = await runUninstall(true, systemUninstallHost(home));
@@ -1485,9 +1487,8 @@ describe("client configuration uninstall preflight", () => {
);
expect(await readFile(cursor, "utf8")).toBe(cursorConfig);
for (const path of managed)
expect(await readFile(path, "utf8")).toBe(
path === managed[0] ? managedSkillManifest : "marker",
);
expect(await readFile(path, "utf8")).toBe("marker");
expect(await readFile(manifest, "utf8")).toBe(ownedSkillManifest);
await writeFile(codex, "");
const repaired = await runUninstall(true, systemUninstallHost(home));
@@ -1495,7 +1496,12 @@ describe("client configuration uninstall preflight", () => {
expect(JSON.parse(await readFile(cursor, "utf8"))).toEqual({
mcpServers: { other: { command: "echo", args: ["hello"] } },
});
for (const path of managed)
// Uninstall removes only the managed bundle, never arbitrary skill files.
expect(await readFile(skillMarker, "utf8")).toBe("marker");
await expect(readFile(manifest, "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
for (const path of managed.slice(1))
await expect(readFile(path, "utf8")).rejects.toThrow();
});
});
@@ -1635,8 +1641,7 @@ describe("client configuration filesystem failure reporting", () => {
roots.push(home);
const skillRoot = join(home, ".agents/skills/reverse-engineer-anything");
await mkdir(skillRoot, { recursive: true });
const manifest = join(skillRoot, "SKILL.md");
await writeFile(manifest, managedSkillManifest);
await writeFile(join(skillRoot, "SKILL.md"), ownedSkillManifest);
const result = await runUninstall(
false,
systemUninstallHost(home, {
@@ -1649,14 +1654,17 @@ describe("client configuration filesystem failure reporting", () => {
name: "skill",
status: "failed",
detail: expect.stringContaining(
`Managed file could not be removed: ${manifest}.`,
`Managed file could not be removed: ${join(skillRoot, "SKILL.md")}`,
),
}),
);
expect(JSON.stringify(result)).not.toContain("SECRET");
expect(await readFile(join(skillRoot, "SKILL.md"), "utf8")).toBe(
ownedSkillManifest,
);
});
it("uninstalls owned skill files across roots while preserving other files", async () => {
it("uninstalls all managed skill roots, including a custom Claude Code directory", async () => {
const home = await createTestTempDirectory("rea-uninstall-skills-");
roots.push(home);
const customClaudeDirectory = join(home, "claude-config");
@@ -1665,12 +1673,12 @@ describe("client configuration filesystem failure reporting", () => {
join(customClaudeDirectory, "skills/reverse-engineer-anything"),
];
await Promise.all(
rootsToRemove.map((path) => mkdir(path, { recursive: true })),
rootsToRemove.map(async (path) => {
await mkdir(path, { recursive: true });
await writeFile(join(path, "SKILL.md"), ownedSkillManifest);
await writeFile(join(path, "user-notes.txt"), "user-authored");
}),
);
for (const path of rootsToRemove) {
await writeFile(join(path, "SKILL.md"), managedSkillManifest);
await writeFile(join(path, "user-notes.txt"), "user-authored notes");
}
const result = await systemUninstallHost(home, testFileSystem, {
CLAUDE_CONFIG_DIR: customClaudeDirectory,
@@ -1679,11 +1687,11 @@ describe("client configuration filesystem failure reporting", () => {
expect(result).toMatchObject({ status: "removed" });
await Promise.all(
rootsToRemove.map(async (path) => {
await expect(readFile(join(path, "SKILL.md"))).rejects.toMatchObject({
code: "ENOENT",
});
await expect(
readFile(join(path, "SKILL.md"), "utf8"),
).rejects.toMatchObject({ code: "ENOENT" });
expect(await readFile(join(path, "user-notes.txt"), "utf8")).toBe(
"user-authored notes",
"user-authored",
);
}),
);
@@ -59,7 +59,10 @@ const expectUnverifiedHostCleanup = (error: ProcessCaptureError): void => {
/^(?:[1-9][0-9]*=(?:environment_unavailable|environment_errno_(?:EACCES|EPERM)))(?:, [1-9][0-9]*=(?:environment_unavailable|environment_errno_(?:EACCES|EPERM)))*$/u,
);
expect(report?.terminal_renderer.state).toBe("cleaned");
expect(report?.temporary_root.state).toBe("cleaned");
expect(report?.temporary_root).toEqual({
state: "unverified",
reason: `temporary root retained because owned process cleanup is unverified: ${report?.owned_process_group.reason}`,
});
};
const captureObservations = (
@@ -169,14 +169,15 @@ itWithCaptureCapability.each(["cli", "mcp"] as const)(
await writeFile(join(root, "z.txt"), "unchanged");
const scenario = {
executable: process.execPath,
// Delete the known file and overflow the remaining directory capacity.
// Incomplete enumeration still cannot prove its absence, regardless of
// which new entry the filesystem returns first.
arguments: [
"-e",
"const fs=require('node:fs');fs.mkdirSync('added');fs.writeFileSync('added/a.txt','added')",
"const fs=require('node:fs');fs.unlinkSync('z.txt');fs.writeFileSync('a.txt','added');fs.writeFileSync('b.txt','added');",
],
working_directory: root,
// Before capture the first root is absent. After capture it consumes
// both entries, so the unchanged second root is certainly unexamined.
filesystem_observation_paths: [join(root, "added"), join(root, "z.txt")],
filesystem_observation_paths: [root],
limits: { files: 2 },
};
const evidence = await (adapter === "cli" ? captureViaCli : captureViaMcp)(
@@ -184,15 +185,17 @@ itWithCaptureCapability.each(["cli", "mcp"] as const)(
);
const capture = parseProcessCapture(evidence.normalized_result);
expect(capture.truncated).toBe(true);
expect(await readFile(join(root, "z.txt"), "utf8")).toBe("unchanged");
await expect(readFile(join(root, "z.txt"), "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
expect(
capture.filesystem_effects.find(({ path }) => path === "root_1:.")
capture.filesystem_effects.find(({ path }) => path === "root_0:z.txt")
?.status,
).toBe("unknown");
expect(
capture.filesystem_checkpoints
.find(({ name }) => name === "after_settlement")
?.effects.find(({ path }) => path === "root_1:.")?.status,
?.effects.find(({ path }) => path === "root_0:z.txt")?.status,
).toBe("unknown");
},
20_000,
@@ -194,13 +194,14 @@ itWithCaptureCapability.each(["cli", "mcp"] as const)(
expect(details.filesystem_after.enumeration_reasons).toContain(
"files_limit",
);
expect(
result.files_before.find(({ path }) => path === "root_0:b.txt")?.sha256,
).toBeNull();
const after = result.files_after.find(
// The truncated subset follows filesystem enumeration order. The large
// file may be retained without a digest, or not enumerated at all.
const largeFile = result.files_after.find(
({ path }) => path === "root_0:b.txt",
);
if (after !== undefined) expect(after.sha256).toBeNull();
if (largeFile !== undefined) expect(largeFile.sha256).toBeNull();
expect(details.filesystem_after.enumeration_truncated).toBe(true);
expect(result.files_after.length).toBeLessThanOrEqual(3);
expect(processSourceTruncated(result, "terminal_rendered")).toBe(true);
expect(compareProcessCaptures(result, result)).toMatchObject({
terminal: "unknown",
@@ -72,13 +72,8 @@ describe("static Electron application analysis", () => {
const result = await reconstructJavaScriptArtifact({ input_path: root });
const graph = parseJavaScriptApplicationGraph(result.graph);
const requestedMembers = graph.nodes.flatMap((node) =>
node.kind === "native-export"
? node.observations.flatMap(({ properties }) => {
const members = properties.requested_members;
return Array.isArray(members) ? members : [];
})
: [],
const nativeBindings = graph.nodes.filter(
(node) => node.kind === "native-export",
);
expect(result.electron_summary).toMatchObject({
@@ -86,7 +81,20 @@ describe("static Electron application analysis", () => {
native_addon_bindings: 1,
resolved_native_addon_bindings: 1,
});
expect(requestedMembers).toEqual(["*"]);
expect(nativeBindings).toMatchObject([
{
observations: [
{
properties: {
requested_members: [],
namespace_access: false,
dynamic_member_access: true,
native_export_verification: "not-performed",
},
},
],
},
]);
});
it("maps Electron boundaries through bundler-renamed bindings", async () => {