From 16d9c5adef163ff69667f4f52ac8f532e29c28dc Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:54:12 +0800 Subject: [PATCH] fix(ci): preserve cleanup boundaries and bounded failure assertions (#1618) Interrupt optional CDP domain shutdown on caller cancellation while preserving an independent bounded detach attempt. Retain original overflow and cleanup failures, verify workspace recovery, preserve user skill files, and reject another unverified firmware worker. Validation: 149 integrated tests; repeated 64 affected boundary tests; full local check. Standalone CI includes real Chrome CLI/stdio MCP and real Web runtime checks. Refs #1613 --- src/browser/CdpTargetSession.ts | 8 ++- .../browser/cdpCleanupCancellation.test.ts | 4 +- .../evm/evmoleInterfaceProvider.test.ts | 53 ++++++++++++++++--- .../qwenClientConfiguration.test.ts | 10 +++- .../firmware/firmwareIntegration.test.ts | 8 +-- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/browser/CdpTargetSession.ts b/src/browser/CdpTargetSession.ts index 4b140e2eda..0c2b5b24f2 100644 --- a/src/browser/CdpTargetSession.ts +++ b/src/browser/CdpTargetSession.ts @@ -51,18 +51,22 @@ export const closeCdpTargetSession = async ( ): Promise => { const { connection, sessionId } = targetSession; const cleanup = new AbortController(); + const domainSignal = + signal === undefined + ? cleanup.signal + : AbortSignal.any([cleanup.signal, signal]); const timeout = setTimeout(() => cleanup.abort(), 1_000); timeout.unref(); try { if (signal?.aborted !== true) { for (const domain of enabledDomains) { - if (cleanup.signal.aborted) break; + if (domainSignal.aborted) break; try { await connection.send( `${domain}.disable`, {}, sessionId, - cleanup.signal, + domainSignal, ); } catch { // Best-effort domain cleanup; transport close is the definitive boundary. diff --git a/tests/boundary/browser/cdpCleanupCancellation.test.ts b/tests/boundary/browser/cdpCleanupCancellation.test.ts index e9508297b5..0d7075ab6b 100644 --- a/tests/boundary/browser/cdpCleanupCancellation.test.ts +++ b/tests/boundary/browser/cdpCleanupCancellation.test.ts @@ -122,10 +122,8 @@ it("closes an Electron inspection session when cleanup is cancelled", async () = operation: "inspect_electron_page", }, }); - expect(browser.commands.map(({ method }) => method)).toContain( - "Page.disable", - ); const methods = browser.commands.map(({ method }) => method); + expect(methods).not.toContain("Page.disable"); expect(methods).toContain("Target.detachFromTarget"); expect(methods).not.toContain("Target.closeTarget"); expect(methods).not.toContain("Browser.close"); diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index 5d1305d21b..708a6fc62d 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -7,6 +7,7 @@ import { projectAnalysisError } from "../../../src/domain/analysisErrorProjectio import { evmInterfaceSchema } from "../../../src/domain/evm/evmInterface.js"; import { EvmoleInterfaceProvider } from "../../../src/evm/EvmoleInterfaceProvider.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; +import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { @@ -128,6 +129,7 @@ it.runIf(!unsupportedHost)( async () => { const { path } = await fixture(); let ownedPath = ""; + let failCleanup = true; const provider = new EvmoleInterfaceProvider({}, async (spawn) => { ownedPath = spawn.cwd ?? ""; const launched = await spawnOwnedProviderProcess({ @@ -135,24 +137,59 @@ it.runIf(!unsupportedHost)( command: process.execPath, arguments: ["-e", 'process.stdout.write("x".repeat(1048577))'], }); + const cleanupOwned = + launched.cleanup ?? + (() => cleanupOwnedProcessGroup(launched.ownership)); return { ...launched, cleanup: async () => { - await launched.cleanup?.(); - throw new Error("injected post-cleanup reporting failure"); + const cleaned = await cleanupOwned(); + if (failCleanup) + throw new Error("injected post-cleanup reporting failure"); + return cleaned; }, }; }); + onTestFinished(async () => { + failCleanup = false; + await provider.close(); + }); const result = await provider.inspect({ path, encoding: "hex" }); if (result.ok) throw new Error("Expected compounded lifecycle failure"); - expect(result.error).toMatchObject({ - cleanupIncomplete: true, - diagnostics: { - reason: "injected post-cleanup reporting failure", - previous_error: { failure_kind: "output-limit" }, - captured_output: { truncated: true, stderr: "" }, + expect(result.error.cleanupIncomplete).toBe(true); + // Inspect the retained error chain without diffing a megabyte of stdout. + const output = z.object({ truncated: z.boolean(), stderr: z.string() }); + const cleanup = z + .object({ + reason: z.string(), + previous_error: z.object({ + code: z.string(), + details: z.object({ + diagnostics: z.object({ + reason: z.string(), + previous_error: z.object({ failure_kind: z.string() }), + captured_output: output, + }), + }), + }), + }) + .parse(projectAnalysisError(result.error).details?.["diagnostics"]); + expect(cleanup).toEqual({ + reason: "Owned EVM worker cleanup could not be confirmed", + previous_error: { + code: "cleanup_incomplete", + details: { + diagnostics: { + reason: "injected post-cleanup reporting failure", + previous_error: { failure_kind: "output-limit" }, + captured_output: { truncated: true, stderr: "" }, + }, + }, }, }); + await access(ownedPath); + failCleanup = false; + await provider.close(); await expect(access(ownedPath)).rejects.toMatchObject({ code: "ENOENT" }); }, ); diff --git a/tests/boundary/filesystem/qwenClientConfiguration.test.ts b/tests/boundary/filesystem/qwenClientConfiguration.test.ts index 0fae29640f..55e8d0ab39 100644 --- a/tests/boundary/filesystem/qwenClientConfiguration.test.ts +++ b/tests/boundary/filesystem/qwenClientConfiguration.test.ts @@ -7,6 +7,7 @@ import { parse as parseJsonc } from "jsonc-parser"; import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; import { configureClientConfiguration } from "../../../src/application/SetupClientConfiguration.js"; import { systemSetupHost } from "../../../src/application/SetupHost.js"; +import { MANAGED_SKILL_FILES } from "../../../src/application/SetupSkill.js"; import { supportedClients } from "../../../src/application/SupportedClients.js"; import { systemUninstallHost } from "../../../src/application/Uninstall.js"; import { PRODUCT_IDENTITY } from "../../../src/identity.js"; @@ -151,6 +152,7 @@ describe("Qwen Code profile setup", () => { }); expect(await setup.installSkill([client.name])).toBe("installed"); const skill = join(home, ".agents", "skills", PRODUCT_IDENTITY.skillName); + await writeFile(join(skill, "user-notes.txt"), "user-authored"); expect(await readFile(join(skill, "SKILL.md"), "utf8")).toContain( "reverse-engineer-anything", ); @@ -173,7 +175,13 @@ describe("Qwen Code profile setup", () => { expect(registered).toEqual(client); expect((await uninstall.removeClient(client)).status).toBe("removed"); expect((await uninstall.removeSkill()).status).toBe("removed"); - await expect(access(skill)).rejects.toMatchObject({ code: "ENOENT" }); + for (const relativePath of MANAGED_SKILL_FILES) + await expect(access(join(skill, relativePath))).rejects.toMatchObject({ + code: "ENOENT", + }); + expect(await readFile(join(skill, "user-notes.txt"), "utf8")).toBe( + "user-authored", + ); if (customHome) await expect(access(join(home, ".qwen"))).rejects.toMatchObject({ code: "ENOENT", diff --git a/tests/boundary/firmware/firmwareIntegration.test.ts b/tests/boundary/firmware/firmwareIntegration.test.ts index 3924f37aba..163ee00e14 100644 --- a/tests/boundary/firmware/firmwareIntegration.test.ts +++ b/tests/boundary/firmware/firmwareIntegration.test.ts @@ -324,7 +324,7 @@ it("cancels a waiting request promptly while keeping later launches behind the a await assertFirmwareCleanup(fixture.launches); }); -it("retains an uncertain workspace and lets the next operation create a new root", async () => { +it("retains an uncertain worker and prevents another launch", async () => { const fixture = await firmwareFixture("cleanup-failure"); const first = await fixture.service.execute("inspect_firmware_regions", { path: fixture.path, @@ -341,12 +341,8 @@ it("retains an uncertain workspace and lets the next operation create a new root path: fixture.path, }); expect(next).toMatchObject({ ok: false, error: { cleanupIncomplete: true } }); - expect(fixture.launches).toHaveLength(2); - const nextWorkspace = fixture.launches[1]?.cwd; - expect(nextWorkspace).toBeDefined(); - expect(nextWorkspace).not.toBe(workspace); + expect(fixture.launches).toHaveLength(1); await access(workspace); - if (nextWorkspace !== undefined) await access(nextWorkspace); }); it("accepts another build on the verified release line and reports it", async () => {