From 741f2de7517d97d26a7cd84cf1045532963eda84 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:29:26 +0800 Subject: [PATCH] test(ci): align source preservation and catalog assertions --- scripts/verify-apk-signature.mjs | 5 +- .../applications/adbListingNumbers.test.ts | 10 ++- .../webArtifactReadOwnership.test.ts | 90 +++++++++++++++++-- .../mcp/packageCatalogVerification.test.ts | 12 ++- 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/scripts/verify-apk-signature.mjs b/scripts/verify-apk-signature.mjs index b6fd2450fa..b5b27e2d74 100644 --- a/scripts/verify-apk-signature.mjs +++ b/scripts/verify-apk-signature.mjs @@ -26,10 +26,7 @@ import { locateApkSigningBlock, parseZipTail, } from "../dist/domain/apkSignature/apkSigningBlock.js"; -import { - mcpTextValue, - requireMcpOutput, -} from "./lib/mcp-verifier-results.mjs"; +import { mcpTextValue, requireMcpOutput } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; const execute = promisify(execFile); diff --git a/tests/acceptance/applications/adbListingNumbers.test.ts b/tests/acceptance/applications/adbListingNumbers.test.ts index e82dc56dd0..297c171325 100644 --- a/tests/acceptance/applications/adbListingNumbers.test.ts +++ b/tests/acceptance/applications/adbListingNumbers.test.ts @@ -27,7 +27,15 @@ const processRow = ( pid: number, ppid: number | null, rss: number | null, -) => ({ user: "shell", pid, ppid, rss, state: "S", name }); +) => ({ + user: "shell", + pid, + ppid, + rss, + state: "S", + // The final NAME column retains the exact suffix emitted by processLine. + name: `${name} \t`, +}); const processListing = (variant: Variant) => { const boundary = Number.MAX_SAFE_INTEGER; diff --git a/tests/boundary/filesystem/webArtifactReadOwnership.test.ts b/tests/boundary/filesystem/webArtifactReadOwnership.test.ts index 0087175e31..397194ac2c 100644 --- a/tests/boundary/filesystem/webArtifactReadOwnership.test.ts +++ b/tests/boundary/filesystem/webArtifactReadOwnership.test.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished, vi } from "vitest"; @@ -55,6 +56,10 @@ import { LocalWebModuleArtifacts } from "../../../src/browser/modules/WebModuleA import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +import { + webArtifactAcquisitionSchema, + type WebArtifactAcquisition, +} from "../../../src/domain/webArtifactAcquisition.js"; import { primaryAnalysisError } from "../../../src/domain/analysisErrorBase.js"; import { webModuleArtifactsFixture } from "../../fixtures/webModuleTrace.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -65,15 +70,24 @@ const sourceFixture = async (source: string) => { const manifestPath = join(root, "manifest.json"); const sourcePath = join(root, "files", "modules", "main.js"); await mkdir(join(root, "files", "modules"), { recursive: true }); - await writeFile(sourcePath, source); - await writeFile(manifestPath, JSON.stringify(fixture.manifest)); + const sourceBytes = Buffer.from(source); + const manifestBytes = Buffer.from(JSON.stringify(fixture.manifest)); + await writeFile(sourcePath, sourceBytes); + await writeFile(manifestPath, manifestBytes); return { input: { manifest_path: manifestPath, script_index: 0 }, sourcePath, - fixture, + sourceBytes, + manifestBytes, }; }; +const fileIdentity = (path: string, bytes: Buffer) => ({ + path, + sha256: createHash("sha256").update(bytes).digest("hex"), + bytes: bytes.length, +}); + it("retains cancelled source read ownership until service close retries it", async () => { onTestFinished(() => { injected.sourcePath = undefined; @@ -85,7 +99,8 @@ it("retains cancelled source read ownership until service close retries it", asy injected.closeAttempts = 0; injected.sourceHandle = undefined; }); - const { input, sourcePath } = await sourceFixture('import "./dep.js";'); + const { input, sourcePath, manifestBytes } = + await sourceFixture('import "./dep.js";'); injected.sourcePath = sourcePath; injected.failClose = true; @@ -109,6 +124,23 @@ it("retains cancelled source read ownership until service close retries it", asy const cancelled = await service.trace(input, { signal: controller.signal }); expect(cancelled.ok).toBe(false); if (cancelled.ok) throw new Error("Expected cancelled source read"); + const expectedAcquisition: WebArtifactAcquisition = { + kind: "web-artifact-acquisition", + analysis: "not-performed", + files: [ + { + role: "manifest", + file: fileIdentity(input.manifest_path, manifestBytes), + admission: "manifest-validated", + }, + ], + }; + expect( + webArtifactAcquisitionSchema.parse(cancelled.error.partialObservation), + ).toEqual(expectedAcquisition); + expect( + projectAnalysisError(cancelled.error).details?.partial_observation, + ).toEqual(expectedAcquisition); expect(cancelled.error.cleanupIncomplete).toBe(true); expect(cancelled.error.cleanupResources).toContain(sourcePath); expect(resolver.resolve).not.toHaveBeenCalled(); @@ -158,6 +190,13 @@ it("retains cancelled source read ownership until service close retries it", asy expect(injected.closeAttempts).toBeGreaterThan(attemptsBeforeRetry); expect(resolver.close).toHaveBeenCalledTimes(3); expect(injected.sourceHandle?.fd).toBe(-1); + expect(primaryAnalysisError(cancelled.error).partialObservation).toEqual( + expectedAcquisition, + ); + expect( + projectAnalysisError(primaryAnalysisError(cancelled.error)).details + ?.partial_observation, + ).toEqual(expectedAcquisition); for (const error of [cancelled.error, blocked.error, cleanupFailure]) { expect(error).toMatchObject({ cleanupIncomplete: false, @@ -176,9 +215,8 @@ it.each([ ])( "keeps physical source read failure and exact cleanup ownership %j", async ({ readFails, closeFails, cancel }) => { - const { input, sourcePath, fixture } = await sourceFixture( - "export const value = 1;", - ); + const { input, sourcePath, sourceBytes, manifestBytes } = + await sourceFixture("export const value = 1;"); const physical = Object.assign(new Error("fixture descriptor EIO"), { code: "EIO", syscall: "read", @@ -212,7 +250,10 @@ it.each([ if (!readFails && !cancel) { if (!result.ok) throw result.error; expect(result.value.normalized_result).toMatchObject({ - source: { path: sourcePath, sha256: fixture.sourceFile.sha256 }, + source: { + path: sourcePath, + sha256: fileIdentity(sourcePath, sourceBytes).sha256, + }, manifest: { path: input.manifest_path }, }); await service.close(); @@ -229,7 +270,34 @@ it.each([ }); else expect(primary).toBeInstanceOf(AnalysisCancelledError); expect(primary.cause).toBe(readFails ? physical : reason); - expect(result.error.partialObservation).toBeUndefined(); + const expectedAcquisition: WebArtifactAcquisition = { + kind: "web-artifact-acquisition", + analysis: "not-performed", + files: [ + { + role: "manifest", + file: fileIdentity(input.manifest_path, manifestBytes), + admission: "manifest-validated", + }, + // Descriptor-finalization cancellation precedes selected-source digest/length admission. + ...(!readFails + ? [ + { + role: "source" as const, + file: fileIdentity(sourcePath, sourceBytes), + admission: "read" as const, + }, + ] + : []), + ], + }; + expect( + webArtifactAcquisitionSchema.parse(result.error.partialObservation), + ).toEqual(expectedAcquisition); + expect(primary.partialObservation).toEqual(expectedAcquisition); + expect( + projectAnalysisError(result.error).details?.partial_observation, + ).toEqual(expectedAcquisition); expect(result.error.cleanupIncomplete).toBe(closeFails); if (closeFails) { expect(result.error.cleanupResources).toEqual([sourcePath]); @@ -242,6 +310,10 @@ it.each([ expect(result.error.cleanupIncomplete).toBe(false); expect(result.error.cleanupResources).toEqual([]); expect(primaryAnalysisError(result.error)).toBe(primary); + expect(primary.partialObservation).toEqual(expectedAcquisition); + expect(projectAnalysisError(primary).details?.partial_observation).toEqual( + expectedAcquisition, + ); expect(projectAnalysisError(result.error)).toMatchObject({ code: readFails ? "artifact_operation_failed" : "cancelled", ...(readFails diff --git a/tests/boundary/mcp/packageCatalogVerification.test.ts b/tests/boundary/mcp/packageCatalogVerification.test.ts index b1fa0066b2..dcfc664394 100644 --- a/tests/boundary/mcp/packageCatalogVerification.test.ts +++ b/tests/boundary/mcp/packageCatalogVerification.test.ts @@ -61,10 +61,20 @@ it("verifies canonical names, converted schemas and session metadata together", })) .filter((group) => group.names.length > 0); toolContract("binary_session").outputSchema.parse(body); - const matchingOmission = { + const disagreeingOmission = { ...status, content: [{ type: "text" as const, text: JSON.stringify(body) }], }; + const matchingOmission = { + ...disagreeingOmission, + structuredContent: body, + }; + await expect( + verifyCompleteToolCatalog({ + ...client, + callTool: async () => disagreeingOmission, + }), + ).rejects.toThrow("binary_session text and structured results disagree"); // Equal inventories are insufficient when both omit the same canonical tool. await expect( verifyCompleteToolCatalog({