mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
fix(session)!: classify unavailable snapshot prerequisites (#7837)
fix(session): classify unavailable snapshot prerequisites
This commit is contained in:
@@ -7,12 +7,14 @@ import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { evidenceBundleForTarget } from "../../domain/evidenceBundle.js";
|
||||
import {
|
||||
EvidenceFileError,
|
||||
EvidenceIntegrityError,
|
||||
AnalysisSnapshotMismatchError,
|
||||
} from "../../domain/evidenceErrors.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
type AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { UnknownRegistryError } from "../../domain/unknownRegistryError.js";
|
||||
import type {
|
||||
RecordUnknownInput,
|
||||
@@ -50,6 +52,19 @@ import {
|
||||
const SNAPSHOT_MUTATION_RECOVERY =
|
||||
"Export session observations through export_evidence_bundle if needed. Close the active target without saving a snapshot, then reopen it before importing or saving a snapshot.";
|
||||
|
||||
/** A missing replay prerequisite is not an Evidence integrity failure. */
|
||||
const snapshotUnavailable = (reason: string) =>
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea",
|
||||
"export_analysis_snapshot",
|
||||
reason,
|
||||
{
|
||||
userMessage: `Analysis snapshot unavailable: ${reason}`,
|
||||
remediationAction:
|
||||
"Export session observations with export_evidence_bundle if needed, or call close_binary without snapshot_path to close the session.",
|
||||
},
|
||||
);
|
||||
|
||||
export interface ActiveAnalysisBinding {
|
||||
readonly target: BinaryTarget;
|
||||
readonly profile: AnalysisProfileCommitment | null;
|
||||
@@ -201,8 +216,8 @@ export abstract class BinarySessionRecords {
|
||||
active?.profile ?? active?.profilelessWorkflowProfile ?? undefined;
|
||||
if (target !== undefined && profile === undefined)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Analysis snapshots require a concrete provider analysis profile",
|
||||
snapshotUnavailable(
|
||||
"The active target has no provider analysis profile for a snapshot.",
|
||||
),
|
||||
);
|
||||
if (
|
||||
@@ -217,8 +232,8 @@ export abstract class BinarySessionRecords {
|
||||
))
|
||||
)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
"Profileless native snapshots may contain only current dispatch workflow observations",
|
||||
snapshotUnavailable(
|
||||
"The active session does not have a supported snapshot binding with replayable workflow observations for this target.",
|
||||
),
|
||||
);
|
||||
return this.#snapshot.export(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { createSingleEntryZip } from "../fixtures/zipNames.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"archive snapshot refusal preserves the session and explains the missing replay binding",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-archive-snapshot-refusal-");
|
||||
const archive = join(root, "sample.zip");
|
||||
const snapshot = join(root, "snapshot.json");
|
||||
const bytes = await createSingleEntryZip("sample.txt");
|
||||
await writeFile(archive, bytes);
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
expect((await call("open_binary", { path: archive })).isError).not.toBe(
|
||||
true,
|
||||
);
|
||||
const failure = parseMcpToolError(
|
||||
await call("close_binary", { snapshot_path: snapshot }),
|
||||
);
|
||||
expect(failure.error).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
retryable: false,
|
||||
message: expect.stringContaining("Analysis snapshot unavailable"),
|
||||
remediation: {
|
||||
action: expect.stringContaining("close_binary without snapshot_path"),
|
||||
},
|
||||
});
|
||||
expect(
|
||||
await access(snapshot).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
).toBe(false);
|
||||
expect((await call("binary_session", {})).structuredContent).toMatchObject({
|
||||
result: { open: true },
|
||||
});
|
||||
expect((await call("close_binary", {})).isError).not.toBe(true);
|
||||
expect((await call("binary_session", {})).structuredContent).toMatchObject({
|
||||
result: { open: false },
|
||||
});
|
||||
expect(
|
||||
Buffer.from(await readFile(archive)).equals(Buffer.from(bytes)),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user