fix(javascript): preserve failure provenance and cleanup ownership
This commit is contained in:
1 parent
957bcfa149
commit
566a9a1551
7 files changed
+175
-69
No files matched your search
@@ -5,6 +5,7 @@ import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js"
|
||||
describe("JavaScript cooperative computation lifecycle", () => {
|
||||
it("observes scheduler cancellation and closes abandoned work", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled computation" };
|
||||
let released = false;
|
||||
function* work(): Generator<void, number> {
|
||||
try {
|
||||
@@ -13,17 +14,18 @@ describe("JavaScript cooperative computation lifecycle", () => {
|
||||
released = true;
|
||||
}
|
||||
}
|
||||
const cancellation = setImmediate().then(() => controller.abort());
|
||||
const cancellation = setImmediate().then(() => controller.abort(reason));
|
||||
await expect(
|
||||
completeJavaScriptAnalysisSteps(work(), controller.signal),
|
||||
).rejects.toMatchObject({ reason: "cancelled" });
|
||||
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
||||
await cancellation;
|
||||
expect(released).toBe(true);
|
||||
});
|
||||
|
||||
it("does not start pre-cancelled work and preserves completed values", async () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const reason = { request: "caller cancelled computation" };
|
||||
controller.abort(reason);
|
||||
let started = false;
|
||||
function* work(): Generator<void, number> {
|
||||
started = true;
|
||||
@@ -32,7 +34,7 @@ describe("JavaScript cooperative computation lifecycle", () => {
|
||||
}
|
||||
await expect(
|
||||
completeJavaScriptAnalysisSteps(work(), controller.signal),
|
||||
).rejects.toMatchObject({ reason: "cancelled" });
|
||||
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
||||
expect(started).toBe(false);
|
||||
expect(await completeJavaScriptAnalysisSteps(work())).toBe(42);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ export const assertJavaScriptAnalysisActive = (signal?: AbortSignal): void => {
|
||||
throw new ArtifactReaderFailure(
|
||||
"cancelled",
|
||||
"JavaScript application analysis cancelled",
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { z } from "zod";
|
||||
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { translateArtifactFailure } from "../../artifacts/ArtifactOperationFailure.js";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
analyzeJavaScriptApplicationInputSchema,
|
||||
@@ -144,7 +145,7 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
result,
|
||||
);
|
||||
return err(
|
||||
artifactFailureToOperationError(cause).retainPartialObservation(
|
||||
translateArtifactFailure(OPERATION, cause).retainPartialObservation(
|
||||
evidence,
|
||||
),
|
||||
);
|
||||
@@ -157,7 +158,7 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
// Preserve execution constraints and their completed partial Evidence.
|
||||
if (cause instanceof AnalysisError) return err(cause);
|
||||
if (cause instanceof ArtifactReaderFailure)
|
||||
return err(artifactFailureToOperationError(cause));
|
||||
return err(translateArtifactFailure(OPERATION, cause));
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(
|
||||
new AnalysisOutputError(OPERATION, describeResultSchemaFailure(cause), {
|
||||
@@ -189,24 +190,6 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
}
|
||||
};
|
||||
|
||||
const artifactFailureToOperationError = (
|
||||
cause: ArtifactReaderFailure,
|
||||
): ArtifactOperationError =>
|
||||
new ArtifactOperationError(
|
||||
OPERATION,
|
||||
cause.reason,
|
||||
cause.details,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
...(cause.cleanup === undefined ? {} : { cleanup: cause.cleanup }),
|
||||
...(cause.partialObservation === undefined
|
||||
? {}
|
||||
: { partialObservation: cause.partialObservation }),
|
||||
...(cause.admission === undefined ? {} : { admission: cause.admission }),
|
||||
},
|
||||
);
|
||||
|
||||
const describeResultSchemaFailure = (cause: z.ZodError): string => {
|
||||
const issue = cause.issues[0];
|
||||
if (issue === undefined) return "Result schema rejected an unknown issue";
|
||||
|
||||
@@ -43,6 +43,8 @@ import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactRefe
|
||||
import type { JavaScriptSourceAnalysisPort } from "../../domain/javascript/javascriptSourceAnalysis.js";
|
||||
import type { JavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { translateArtifactFailure } from "../../artifacts/ArtifactOperationFailure.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
@@ -153,13 +155,16 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
if (signal?.aborted === true) {
|
||||
interruption = new AnalysisCancelledError(
|
||||
"analyze_javascript_application",
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("analyze_javascript_application");
|
||||
throw new AnalysisCancelledError("analyze_javascript_application", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
await beforeFile(file, selectedFileIndex++, selectedFileCount);
|
||||
analysisStarted = true;
|
||||
if (file.kind !== "javascript" || !file.text.included) {
|
||||
@@ -245,39 +250,44 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
break;
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
if (signal?.aborted !== true && !(cause instanceof AnalysisError))
|
||||
throw cause;
|
||||
interruption =
|
||||
signal?.aborted === true
|
||||
? new AnalysisCancelledError("analyze_javascript_application")
|
||||
: cause instanceof AnalysisError
|
||||
? cause
|
||||
: null;
|
||||
if (interruption !== null) {
|
||||
const scope = javascriptArtifactAnalysisScopes[file.kind];
|
||||
state.files.push({
|
||||
file,
|
||||
javascript: null,
|
||||
semantic: null,
|
||||
...(scope === null
|
||||
? {}
|
||||
: {
|
||||
analysis_failure: {
|
||||
scope,
|
||||
status: analysisStarted ? "interrupted" : "not-started",
|
||||
reason: `${file.path}: ${interruption.message}`,
|
||||
error: projectAnalysisError(interruption),
|
||||
limits: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
projection.recordUnavailableFile(
|
||||
file,
|
||||
interruption.message,
|
||||
null,
|
||||
"incomplete-module",
|
||||
if (cause instanceof AnalysisError) interruption = cause;
|
||||
else if (
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
cause.reason === "cancelled"
|
||||
)
|
||||
interruption = translateArtifactFailure(
|
||||
"analyze_javascript_application",
|
||||
cause,
|
||||
);
|
||||
}
|
||||
else if (signal?.aborted === true && cause === signal.reason)
|
||||
interruption = new AnalysisCancelledError(
|
||||
"analyze_javascript_application",
|
||||
{ cause },
|
||||
);
|
||||
else throw cause;
|
||||
const scope = javascriptArtifactAnalysisScopes[file.kind];
|
||||
state.files.push({
|
||||
file,
|
||||
javascript: null,
|
||||
semantic: null,
|
||||
...(scope === null
|
||||
? {}
|
||||
: {
|
||||
analysis_failure: {
|
||||
scope,
|
||||
status: analysisStarted ? "interrupted" : "not-started",
|
||||
reason: `${file.path}: ${interruption.message}`,
|
||||
error: projectAnalysisError(interruption),
|
||||
limits: null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
projection.recordUnavailableFile(
|
||||
file,
|
||||
interruption.message,
|
||||
null,
|
||||
"incomplete-module",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,13 @@ export const reconstructJavaScriptArtifact = async (
|
||||
const checkpointCompletedAnalysis = async (): Promise<void> => {
|
||||
await setImmediate();
|
||||
if (interruption == null && signal?.aborted === true)
|
||||
interruption = new ArtifactOperationError(OPERATION, "cancelled");
|
||||
interruption = new ArtifactOperationError(
|
||||
OPERATION,
|
||||
"cancelled",
|
||||
undefined,
|
||||
undefined,
|
||||
{ cause: signal.reason },
|
||||
);
|
||||
};
|
||||
const reportCompletedPhase = async (
|
||||
phase: string,
|
||||
|
||||
+6
-2
@@ -30,6 +30,7 @@ describe("JavaScript source selection cancellation boundary", () => {
|
||||
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
||||
const reader = createJavaScriptArtifactReader(root, "directory");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled selected sources" };
|
||||
const sourcePaths = ["main.mjs"];
|
||||
try {
|
||||
const files = await readJavaScriptArtifactFiles(
|
||||
@@ -52,7 +53,7 @@ describe("JavaScript source selection cancellation boundary", () => {
|
||||
{
|
||||
analyze: async (...args) => {
|
||||
const result = await realWorker(...args);
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
@@ -64,6 +65,7 @@ describe("JavaScript source selection cancellation boundary", () => {
|
||||
await worker.close();
|
||||
}
|
||||
expect(outcome.interruption).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(outcome.interruption?.cause).toBe(reason);
|
||||
const unselected = outcome.analysis.files.filter(
|
||||
({ file }) =>
|
||||
!file.text.included && file.text.reason === "outside-analysis-scope",
|
||||
@@ -151,6 +153,7 @@ describe("Selected JavaScript source failure diagnostics", () => {
|
||||
await writeJavaScriptSourceSelectionFixture(root);
|
||||
await writeFile(join(root, "z-later.mjs"), "export const later = 1;\n");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled selected sources" };
|
||||
const source_paths = ["main.mjs", "z-later.mjs"];
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, source_paths },
|
||||
@@ -162,12 +165,13 @@ describe("Selected JavaScript source failure diagnostics", () => {
|
||||
event.phase === "parse_javascript_source" &&
|
||||
event.message.includes("z-later.mjs")
|
||||
)
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Expected selected-source cancellation");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(projectAnalysisError(result.error).code).toBe("cancelled");
|
||||
const evidence = parseEvidence(result.error.partialObservation);
|
||||
const partial = javascriptApplicationAnalysisResultSchema.parse(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setImmediate } from "node:timers";
|
||||
import { mkdir, realpath, symlink, writeFile } from "node:fs/promises";
|
||||
import { mkdir, open, realpath, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
@@ -11,6 +11,7 @@ import { analyzeJavaScriptApplication as analyzeOwnedApplication } from "../../.
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
|
||||
|
||||
it.each(["during cleanup", "on the next control turn"] as const)(
|
||||
@@ -31,12 +32,13 @@ it.each(["during cleanup", "on the next control turn"] as const)(
|
||||
'import { readFile } from "node:fs/promises"; export { readFile };\n',
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
class CancellingCleanupScope extends ArtifactResourceScope {
|
||||
override async release(owner: ArtifactResourceOwner) {
|
||||
const result = await super.release(owner);
|
||||
if (owner.kind === "javascript-source-analysis") {
|
||||
if (timing === "during cleanup") controller.abort();
|
||||
else setImmediate(() => controller.abort());
|
||||
if (timing === "during cleanup") controller.abort(reason);
|
||||
else setImmediate(() => controller.abort(reason));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -49,6 +51,7 @@ it.each(["during cleanup", "on the next control turn"] as const)(
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.ok) throw new Error("Cancelled handoff must not succeed");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe("cancelled");
|
||||
expect(projected).toMatchObject({
|
||||
@@ -111,6 +114,7 @@ it("preserves imported manifest status when cancelled before visiting manifests"
|
||||
);
|
||||
await writeFile(join(root, "b.cjs"), "module.exports = 1;");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
const visited: string[] = [];
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
@@ -120,12 +124,13 @@ it("preserves imported manifest status when cancelled before visiting manifests"
|
||||
async report(event) {
|
||||
if (event.phase !== "parse_javascript_source") return;
|
||||
visited.push(event.message);
|
||||
if (event.message.includes("b.cjs")) controller.abort();
|
||||
if (event.message.includes("b.cjs")) controller.abort(reason);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Cancelled analysis must return a failure");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
const failure = projectAnalysisError(result.error);
|
||||
expect(failure.code).toBe("cancelled");
|
||||
expect(visited).toHaveLength(2);
|
||||
@@ -171,6 +176,7 @@ it("keeps cancellation before native metadata out of AST scopes and semantic mod
|
||||
).map(([path, content]) => writeFile(join(root, path), content)),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
let reachedNativeMetadata = false;
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
@@ -183,7 +189,7 @@ it("keeps cancellation before native metadata out of AST scopes and semantic mod
|
||||
event.message.includes("b.node")
|
||||
) {
|
||||
reachedNativeMetadata = true;
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -191,6 +197,7 @@ it("keeps cancellation before native metadata out of AST scopes and semantic mod
|
||||
);
|
||||
expect(reachedNativeMetadata).toBe(true);
|
||||
if (result.ok) throw new Error("Cancelled analysis must return a failure");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
const partial = z
|
||||
.object({
|
||||
code: z.literal("cancelled"),
|
||||
@@ -261,6 +268,7 @@ it("interrupts semantic graph commitment before result validation", async () =>
|
||||
).join("\n"),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
const phases: string[] = [];
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
@@ -270,7 +278,7 @@ it("interrupts semantic graph commitment before result validation", async () =>
|
||||
async report(event) {
|
||||
phases.push(event.phase);
|
||||
if (event.phase === "seal_javascript_semantic_graph")
|
||||
setImmediate(() => controller.abort());
|
||||
setImmediate(() => controller.abort(reason));
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -298,13 +306,14 @@ it("honors control turns and cancellation while preparing one large source for o
|
||||
).join("\n"),
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
const phases: string[] = [];
|
||||
let turns = 0;
|
||||
// Count event-loop turns once the file starts; parsing, analysis and
|
||||
// projection of this one file must leave room for them.
|
||||
const tick = (): void => {
|
||||
turns += 1;
|
||||
if (turns === 3) controller.abort();
|
||||
if (turns === 3) controller.abort(reason);
|
||||
else if (!controller.signal.aborted) setImmediate(tick);
|
||||
};
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
@@ -345,6 +354,7 @@ it.each([
|
||||
"export function inspect(value) { return JSON.stringify(value); }\n",
|
||||
);
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
let requested = false;
|
||||
const terminal: string[] = [];
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
@@ -356,7 +366,7 @@ it.each([
|
||||
if (event.terminal) terminal.push(event.phase);
|
||||
if (event.phase === phase) {
|
||||
requested = true;
|
||||
setImmediate(() => controller.abort());
|
||||
setImmediate(() => controller.abort(reason));
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -364,6 +374,7 @@ it.each([
|
||||
);
|
||||
expect(requested).toBe(true);
|
||||
if (result.ok) throw new Error("Cancelled analysis must return a failure");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
const failure = projectAnalysisError(result.error);
|
||||
expect(failure).toMatchObject({ code: "cancelled" });
|
||||
expect(failure).toMatchObject({
|
||||
@@ -390,18 +401,20 @@ it("honors cancellation observed during the terminal publication race", async ()
|
||||
const root = await createTestTempDirectory("rea-js-cancel-terminal-");
|
||||
await writeFile(join(root, "main.js"), "export const value = 1;\n");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "caller cancelled JavaScript analysis" };
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
{
|
||||
signal: controller.signal,
|
||||
progress: {
|
||||
async report(event) {
|
||||
if (event.terminal) controller.abort();
|
||||
if (event.terminal) controller.abort(reason);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Expected cancellation before admission");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
const failure = projectAnalysisError(result.error);
|
||||
expect(failure).toMatchObject({
|
||||
details: { reason: "cancelled" },
|
||||
@@ -416,3 +429,90 @@ it("honors cancellation observed during the terminal publication race", async ()
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the exact retained descriptor owner through application failure translation and retry", async () => {
|
||||
const root = await createTestTempDirectory("rea-js-retained-cleanup-");
|
||||
const paths = [join(root, "first.js"), join(root, "second.js")];
|
||||
await Promise.all(
|
||||
paths.map((path) => writeFile(path, "export const value = 1;")),
|
||||
);
|
||||
const files = await Promise.all(paths.map((path) => open(path, "r")));
|
||||
const scope = new ArtifactResourceScope();
|
||||
let allowClose = false;
|
||||
const attempts = [0, 0];
|
||||
files.forEach((file, index) =>
|
||||
scope.retain({
|
||||
kind: "lease",
|
||||
resource: paths[index]!,
|
||||
lease: {
|
||||
async release() {
|
||||
attempts[index]! += 1;
|
||||
if (index === 0 && !allowClose)
|
||||
throw new Error("injected descriptor close denial");
|
||||
await file.close();
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
onTestFinished(async () => {
|
||||
allowClose = true;
|
||||
await scope.close();
|
||||
});
|
||||
const result = await analyzeOwnedApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
scope,
|
||||
);
|
||||
if (result.ok) throw new Error("Expected retained cleanup to block analysis");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
retryable: false,
|
||||
});
|
||||
expect(attempts).toEqual([1, 1]);
|
||||
allowClose = true;
|
||||
await scope.close();
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(attempts).toEqual([2, 1]);
|
||||
});
|
||||
|
||||
it.each(["native-error", "analysis-error", "abort-reason"] as const)(
|
||||
"classifies the producer progress failure under late cancellation (%s)",
|
||||
async (kind) => {
|
||||
const root = await createTestTempDirectory("rea-js-progress-primary-");
|
||||
await writeFile(join(root, "main.js"), "export const observed = 1;");
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "unrelated cancellation" };
|
||||
const failure =
|
||||
kind === "abort-reason"
|
||||
? reason
|
||||
: kind === "analysis-error"
|
||||
? new AnalysisOutputError(
|
||||
"analyze_javascript_application",
|
||||
"progress callback failed",
|
||||
)
|
||||
: new Error("progress callback failed");
|
||||
const result = await analyzeJavaScriptApplication(
|
||||
{ input_path: root, format: "directory" },
|
||||
{
|
||||
signal: controller.signal,
|
||||
progress: {
|
||||
async report(event) {
|
||||
if (event.phase === "parse_javascript_source") {
|
||||
controller.abort(reason);
|
||||
throw failure;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Expected progress failure");
|
||||
if (kind === "abort-reason") {
|
||||
expect(projectAnalysisError(result.error).code).toBe("cancelled");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
} else if (kind === "analysis-error") expect(result.error).toBe(failure);
|
||||
else {
|
||||
expect(result.error).toMatchObject({ _tag: "ProviderAdapterError" });
|
||||
expect(result.error.cause).toBe(failure);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user