fix(session): align resource-only status and snapshot recovery (#7896)

* fix(session): align resource-only status and snapshot recovery

* fix(session): report independent workflows during resource sessions

* test(mcp): honor SQLite runtime availability

* test: compare resource snapshot guidance with its formatter

Assert projected error codes and the owning remediation action for
resource-session snapshot refusals, and type MCP error text as a closed
content item.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-30 00:03:19 +08:00
1 parent bf115be2cd
commit 845080aef6
8 files changed
+260 -23

No files matched your search

+3 -11
View File
@@ -64,7 +64,7 @@ import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancell
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { profilelessNativeDispatchProfile } from "./ProfilelessNativeWorkflowSnapshot.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js";
import { ResourceSnapshotMismatchError } from "../../domain/evidenceErrors.js";
import {
parseBinaryTarget,
type BinaryTargetResolverOptions,
@@ -1149,21 +1149,13 @@ export class BinarySession
override exportAnalysisSnapshot() {
if (this.#activeBundleResources !== undefined)
return err(
new AnalysisSnapshotMismatchError(
"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.",
),
);
return err(new ResourceSnapshotMismatchError("export"));
return super.exportAnalysisSnapshot();
}
override importAnalysisSnapshot(snapshot: AnalysisSnapshot) {
if (this.#activeBundleResources !== undefined)
return err(
new AnalysisSnapshotMismatchError(
"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.",
),
);
return err(new ResourceSnapshotMismatchError("import"));
return super.importAnalysisSnapshot(snapshot);
}
@@ -14,7 +14,10 @@ import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { BinarySession } from "./BinarySession.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
import { bundleResourceRootSchema } from "../../domain/apple/bundleResourceRoot.js";
import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorRemediationAction } from "../../domain/analysisErrorPresentation.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { ResourceSnapshotMismatchError } from "../../domain/evidenceErrors.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
@@ -36,6 +39,24 @@ const binaryIdentity: ProviderIdentity = {
};
const temporaryDirectories: string[] = [];
const expectResourceSnapshotRefusal = (
error: AnalysisError,
operation: "export" | "import",
): void => {
expect(error).toBeInstanceOf(ResourceSnapshotMismatchError);
expect(error).toMatchObject({
_tag: "EvidenceIntegrityError",
operation,
});
const projected = projectAnalysisError(error);
expect(projected.code).toBe("evidence_integrity_mismatch");
expect(projected.category).toBe("integrity_mismatch");
expect(projected.retryable).toBe(false);
expect(projected.remediation).toEqual({
action: analysisErrorRemediationAction(error),
});
};
afterEach(async () => {
await Promise.all(
temporaryDirectories
@@ -137,13 +158,17 @@ it("opens a resource-only bundle without executable identity and keeps snapshot
resource_root: { canonical_path: opened.value.canonicalPath },
});
const snapshot = session.exportAnalysisSnapshot();
expect(snapshot.ok).toBe(false);
if (!snapshot.ok)
expect(snapshot.error).toBeInstanceOf(AnalysisSnapshotMismatchError);
if (snapshot.ok)
throw new Error("resource session exported a binary snapshot");
expectResourceSnapshotRefusal(snapshot.error, "export");
const imported = session.importAnalysisSnapshot({} as AnalysisSnapshot);
expect(imported.ok).toBe(false);
if (!imported.ok)
expect(imported.error).toBeInstanceOf(AnalysisSnapshotMismatchError);
if (imported.ok)
throw new Error("resource session imported a binary snapshot");
expectResourceSnapshotRefusal(imported.error, "import");
expect(await session.status()).toMatchObject({
open: true,
session_kind: "bundle_resources",
});
const closedResult = await session.close();
expect(closedResult.ok).toBe(true);
expect(closed).toBe(true);
@@ -24,6 +24,13 @@ export const BUNDLE_RESOURCE_OPERATIONS = new Set<AnalysisOperation>([
...BUNDLE_ARTIFACT_RESOURCE_OPERATIONS,
]);
/** Share the dispatch owner's resource-session operation policy with discovery. */
export const isBundleResourceOperation = (name: string): boolean => {
for (const operation of BUNDLE_RESOURCE_OPERATIONS)
if (operation === name) return true;
return false;
};
/** Provider-specific owner of the four read-only app-resource operations. */
export interface BundleResourceClientFactory {
createClient(