test(observation): share incomplete-observation assertions
The new non-JSON cases repeated the same owner-error checks and pushed duplication over the 3.5% limit. One expectation helper keeps those checks without a second copy. Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
6 files changed
+153
-211
No files matched your search
@@ -8,6 +8,11 @@ import {
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
expectDroppedPartial,
|
||||
expectKeptPartial,
|
||||
expectOwnerError,
|
||||
} from "./ownerObservationExpectation.js";
|
||||
import { BrowserObservationError } from "../domain/browserObservationError.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { passiveNetworkCapture } from "../../tests/fixtures/passiveNetworkCapture.js";
|
||||
@@ -243,16 +248,7 @@ it("keeps page cancellation when the retained inspection is incomplete", async (
|
||||
include_script_sources: false,
|
||||
});
|
||||
const result = await inspectWebPage(browser, pageInput);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expectKeptPartial(expectOwnerError(result, failure, reason), partial);
|
||||
});
|
||||
|
||||
it("keeps bundle cancellation and an earlier failure when retained facts are incomplete", async () => {
|
||||
@@ -278,16 +274,13 @@ it("keeps bundle cancellation and an earlier failure when retained facts are inc
|
||||
analyzeBundle: async () => err(failure),
|
||||
} as unknown as BrowserObservationPort;
|
||||
const result = await analyzeWebBundle(browser, selected);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the owner failure");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expectKeptPartial(
|
||||
expectOwnerError(result, failure, failure.cause),
|
||||
partial,
|
||||
failure === timedOut ? "provider_timeout" : "cancelled",
|
||||
);
|
||||
}
|
||||
expect(projectAnalysisError(cancelled)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expect(projectAnalysisError(cancelled).code).toBe("cancelled");
|
||||
});
|
||||
|
||||
const pageInput = () =>
|
||||
@@ -347,22 +340,12 @@ it.each([
|
||||
operation === "inspect_web_page"
|
||||
? await inspectWebPage(browserReturning(failure), pageInput())
|
||||
: await analyzeWebBundle(browserReturning(failure), bundleInput());
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures).toMatchObject([
|
||||
{
|
||||
_tag: "AnalysisOutputError",
|
||||
operation,
|
||||
reason: "Browser provider retained a non-JSON incomplete observation.",
|
||||
},
|
||||
]);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expect(projectAnalysisError(result.error).details).not.toHaveProperty(
|
||||
const error = expectOwnerError(result, failure, reason);
|
||||
expectDroppedPartial(
|
||||
error,
|
||||
"Browser provider retained a non-JSON incomplete observation.",
|
||||
);
|
||||
expect(projectAnalysisError(error).details).not.toHaveProperty(
|
||||
"partial_observation",
|
||||
);
|
||||
},
|
||||
@@ -387,17 +370,16 @@ it.each([
|
||||
const failure = new BrowserObservationError("inspect_web_page", "timeout", {
|
||||
partialObservation: partial as never,
|
||||
});
|
||||
const result = await inspectWebPage(browserReturning(failure), pageInput());
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected timeout");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures[0]).toBeInstanceOf(
|
||||
AnalysisOutputError,
|
||||
const error = expectOwnerError(
|
||||
await inspectWebPage(browserReturning(failure), pageInput()),
|
||||
failure,
|
||||
undefined,
|
||||
);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
category: "timeout",
|
||||
});
|
||||
expectDroppedPartial(
|
||||
error,
|
||||
"Browser provider retained a non-JSON incomplete observation.",
|
||||
"provider_timeout",
|
||||
);
|
||||
expect(projectAnalysisError(error).category).toBe("timeout");
|
||||
},
|
||||
);
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { BrowserObservationError } from "../domain/browserObservationError.js";
|
||||
import { parseEvidence } from "../domain/evidence.js";
|
||||
import { browserScenarioInputSchema } from "../domain/browserScenario.js";
|
||||
@@ -16,6 +15,11 @@ import { err, ok } from "../domain/result.js";
|
||||
import { scriptScenarioFixture } from "../../tests/fixtures/webScriptCapture.js";
|
||||
import type { BrowserScenarioCapturePort } from "./BrowserScenarioCapturePort.js";
|
||||
import { captureBrowserScenario } from "./BrowserScenarioCaptureService.js";
|
||||
import {
|
||||
expectDroppedPartial,
|
||||
expectKeptPartial,
|
||||
expectOwnerError,
|
||||
} from "./ownerObservationExpectation.js";
|
||||
|
||||
const scenario = browserScenarioInputSchema.parse({
|
||||
browser: { mode: "launch", executable_path: "/fixture/chrome" },
|
||||
@@ -138,16 +142,7 @@ it("keeps startup cancellation and its incomplete capture", async () => {
|
||||
captureScenario: async () => err(failure),
|
||||
};
|
||||
const result = await captureBrowserScenario(provider, scenario);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expectKeptPartial(expectOwnerError(result, failure, reason), partial);
|
||||
});
|
||||
|
||||
it.each([{ steps: [undefined] }, { started_at: Number.NaN }])(
|
||||
@@ -167,22 +162,10 @@ it.each([{ steps: [undefined] }, { started_at: Number.NaN }])(
|
||||
captureScenario: async () => err(failure),
|
||||
};
|
||||
const result = await captureBrowserScenario(provider, scenario);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures).toMatchObject([
|
||||
{
|
||||
_tag: "AnalysisOutputError",
|
||||
operation: "capture_browser_scenario",
|
||||
reason:
|
||||
"Browser scenario provider retained a non-JSON incomplete observation.",
|
||||
},
|
||||
]);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expectDroppedPartial(
|
||||
expectOwnerError(result, failure, reason),
|
||||
"Browser scenario provider retained a non-JSON incomplete observation.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import {
|
||||
expectDroppedPartial,
|
||||
expectKeptPartial,
|
||||
expectOwnerError,
|
||||
} from "./ownerObservationExpectation.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { parseEvidence } from "../domain/evidence.js";
|
||||
|
||||
it("rejects malformed input and prior cancellation without calling a provider", async () => {
|
||||
@@ -166,6 +172,32 @@ it("retains completed execution Evidence on late abort and keeps invalid output
|
||||
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
const callRuntime = (
|
||||
operation: "observe" | "inspect",
|
||||
failure: AnalysisError,
|
||||
) => {
|
||||
const service = new WebRuntimeService({
|
||||
identity: () => ({
|
||||
id: "web-runtime",
|
||||
name: "Runtime fixture",
|
||||
version: "1",
|
||||
}),
|
||||
observeExecution: async () => err(failure),
|
||||
inspectEventListeners: async () => err(failure),
|
||||
});
|
||||
return operation === "observe"
|
||||
? service.observe({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
observation_ms: 5,
|
||||
})
|
||||
: service.inspect({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
selector: "#selected",
|
||||
});
|
||||
};
|
||||
|
||||
it.each([
|
||||
{
|
||||
operation: "observe" as const,
|
||||
@@ -191,37 +223,10 @@ it.each([
|
||||
: "inspect_web_event_listeners",
|
||||
{ cause: reason, partialObservation: partial as never },
|
||||
);
|
||||
const service = new WebRuntimeService({
|
||||
identity: () => ({
|
||||
id: "web-runtime",
|
||||
name: "Runtime fixture",
|
||||
version: "1",
|
||||
}),
|
||||
observeExecution: async () => err(failure),
|
||||
inspectEventListeners: async () => err(failure),
|
||||
});
|
||||
const result =
|
||||
operation === "observe"
|
||||
? await service.observe({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
observation_ms: 5,
|
||||
})
|
||||
: await service.inspect({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
selector: "#selected",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expectKeptPartial(
|
||||
expectOwnerError(await callRuntime(operation, failure), failure, reason),
|
||||
partial,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -253,38 +258,10 @@ it.each([
|
||||
: "inspect_web_event_listeners",
|
||||
{ cause: reason, partialObservation: partial as never },
|
||||
);
|
||||
const service = new WebRuntimeService({
|
||||
identity: () => ({
|
||||
id: "web-runtime",
|
||||
name: "Runtime fixture",
|
||||
version: "1",
|
||||
}),
|
||||
observeExecution: async () => err(failure),
|
||||
inspectEventListeners: async () => err(failure),
|
||||
});
|
||||
const result =
|
||||
operation === "observe"
|
||||
? await service.observe({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
observation_ms: 5,
|
||||
})
|
||||
: await service.inspect({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
selector: "#selected",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures).toMatchObject([
|
||||
{ _tag: "AnalysisOutputError", reason: outputReason },
|
||||
]);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expectDroppedPartial(
|
||||
expectOwnerError(await callRuntime(operation, failure), failure, reason),
|
||||
outputReason,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -295,27 +272,15 @@ it("drops a non-JSON execution partial and keeps timeout primary", async () => {
|
||||
window: { requested_ms: 5 },
|
||||
} as never,
|
||||
});
|
||||
const service = new WebRuntimeService({
|
||||
identity: () => ({
|
||||
id: "web-runtime",
|
||||
name: "Runtime fixture",
|
||||
version: "1",
|
||||
}),
|
||||
observeExecution: async () => err(failure),
|
||||
inspectEventListeners: async () => err(failure),
|
||||
});
|
||||
const result = await service.observe({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "selected-page",
|
||||
observation_ms: 5,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected timeout");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures[0]).toBeInstanceOf(AnalysisOutputError);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
category: "timeout",
|
||||
});
|
||||
const error = expectOwnerError(
|
||||
await callRuntime("observe", failure),
|
||||
failure,
|
||||
undefined,
|
||||
);
|
||||
expectDroppedPartial(
|
||||
error,
|
||||
"Provider retained a non-JSON incomplete execution observation.",
|
||||
"provider_timeout",
|
||||
);
|
||||
expect(projectAnalysisError(error).category).toBe("timeout");
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
@@ -19,6 +18,11 @@ import {
|
||||
inspectElectronPage,
|
||||
listElectronTargets,
|
||||
} from "./ElectronObservationService.js";
|
||||
import {
|
||||
expectDroppedPartial,
|
||||
expectKeptPartial,
|
||||
expectOwnerError,
|
||||
} from "../ownerObservationExpectation.js";
|
||||
|
||||
const identity = { id: "electron-fixture", name: "Fixture", version: "1" };
|
||||
const input = inspectElectronPageInputSchema.parse({
|
||||
@@ -264,16 +268,7 @@ it("keeps cancellation when the retained Electron inspection is incomplete", asy
|
||||
inspectPage: async () => err(failure),
|
||||
};
|
||||
const result = await inspectElectronPage(provider, input);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expectKeptPartial(expectOwnerError(result, failure, reason), partial);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -300,20 +295,9 @@ it.each([
|
||||
inspectPage: async () => err(failure),
|
||||
};
|
||||
const result = await inspectElectronPage(provider, input);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures).toMatchObject([
|
||||
{
|
||||
_tag: "AnalysisOutputError",
|
||||
operation: "inspect_electron_page",
|
||||
reason: "Electron provider retained a non-JSON incomplete observation.",
|
||||
},
|
||||
]);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expectDroppedPartial(
|
||||
expectOwnerError(result, failure, reason),
|
||||
"Electron provider retained a non-JSON incomplete observation.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import type { JavaScriptRuntimeTargetList } from "../../domain/javascript/javascriptRuntimeObservation.js";
|
||||
@@ -15,6 +14,11 @@ import {
|
||||
listJavaScriptRuntimeTargets,
|
||||
observeJavaScriptRuntime,
|
||||
} from "./JavaScriptRuntimeObservationService.js";
|
||||
import {
|
||||
expectDroppedPartial,
|
||||
expectKeptPartial,
|
||||
expectOwnerError,
|
||||
} from "../ownerObservationExpectation.js";
|
||||
|
||||
it("preserves failed Inspector cleanup when cancellation occurs during observation", async () => {
|
||||
const controller = new AbortController();
|
||||
@@ -337,16 +341,7 @@ it("keeps cancellation when the retained runtime observation is incomplete", asy
|
||||
target_id: "target",
|
||||
observation_ms: 0,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBe(partial);
|
||||
expect(result.error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
expectKeptPartial(expectOwnerError(result, failure, reason), partial);
|
||||
});
|
||||
|
||||
it.each([{ scripts: [undefined] }, { elapsed_ms: Number.NaN }])(
|
||||
@@ -368,20 +363,9 @@ it.each([{ scripts: [undefined] }, { elapsed_ms: Number.NaN }])(
|
||||
target_id: "target",
|
||||
observation_ms: 0,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toBeUndefined();
|
||||
expect(result.error.secondaryFailures).toMatchObject([
|
||||
{
|
||||
_tag: "AnalysisOutputError",
|
||||
operation: "observe_javascript_runtime",
|
||||
reason: "Inspector retained a non-JSON incomplete observation.",
|
||||
},
|
||||
]);
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cancelled",
|
||||
});
|
||||
expectDroppedPartial(
|
||||
expectOwnerError(result, failure, reason),
|
||||
"Inspector retained a non-JSON incomplete observation.",
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect } from "vitest";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
|
||||
/** Assert the service returned the same owner error and its cause. */
|
||||
export const expectOwnerError = <T>(
|
||||
result: Result<T, AnalysisError>,
|
||||
failure: AnalysisError,
|
||||
cause: unknown,
|
||||
): AnalysisError => {
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected the owner failure");
|
||||
expect(result.error).toBe(failure);
|
||||
expect(result.error.cause).toBe(cause);
|
||||
return result.error;
|
||||
};
|
||||
|
||||
/** A JSON-safe incomplete observation stays on the owner error. */
|
||||
export const expectKeptPartial = (
|
||||
error: AnalysisError,
|
||||
partial: unknown,
|
||||
code: "cancelled" | "provider_timeout" = "cancelled",
|
||||
): void => {
|
||||
expect(error.partialObservation).toBe(partial);
|
||||
expect(error.secondaryFailures).toHaveLength(0);
|
||||
expect(projectAnalysisError(error)).toMatchObject({
|
||||
code,
|
||||
details: { partial_observation: partial },
|
||||
});
|
||||
};
|
||||
|
||||
/** A non-JSON partial is cleared and recorded as a secondary output failure. */
|
||||
export const expectDroppedPartial = (
|
||||
error: AnalysisError,
|
||||
reason: string,
|
||||
code: "cancelled" | "provider_timeout" = "cancelled",
|
||||
): void => {
|
||||
expect(error.partialObservation).toBeUndefined();
|
||||
expect(error.secondaryFailures).toMatchObject([
|
||||
{ _tag: "AnalysisOutputError", reason },
|
||||
]);
|
||||
expect(projectAnalysisError(error).code).toBe(code);
|
||||
};
|
||||
Reference in new issue
Block a user