mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(ida): preserve startup failure ownership
This commit is contained in:
@@ -95,6 +95,49 @@ const fixture = async (
|
||||
return { root, target, producer, client };
|
||||
};
|
||||
|
||||
it.each(["initialize", "transport"])(
|
||||
"preserves producer outcome facts when attributing a cached %s failure",
|
||||
async (operation) => {
|
||||
const { producer, client } = await fixture();
|
||||
const primary = new AnalysisCancelledError("provider_startup");
|
||||
const source = new ProviderAdapterError("ida", operation, {
|
||||
cleanupPrimary: primary,
|
||||
limit: {
|
||||
setting: "regex.matching_deadline_ms",
|
||||
effective_value: 100,
|
||||
unit: "ms",
|
||||
minimum: 1,
|
||||
maximum: 1000,
|
||||
applies_at: "next_operation",
|
||||
agent_editable: true,
|
||||
scope: "rea-runtime",
|
||||
rea_restart_required: false,
|
||||
},
|
||||
diagnostics: { producer_fact: "retained" },
|
||||
});
|
||||
const connect = vi.spyOn(producer, "connect").mockRejectedValue(source);
|
||||
for (const requested of ["list_procedures", "list_strings"] as const) {
|
||||
const result = await client.execute(requested, {});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected startup failure");
|
||||
expect(result.error).toBeInstanceOf(ProviderAdapterError);
|
||||
expect(result.error).toMatchObject({
|
||||
operation: requested,
|
||||
limit: source.limit,
|
||||
cleanupPrimary: { operation: requested },
|
||||
diagnostics: { producer_fact: "retained" },
|
||||
});
|
||||
expect(result.error.cleanupPrimary).toBeInstanceOf(
|
||||
AnalysisCancelledError,
|
||||
);
|
||||
}
|
||||
expect(connect).toHaveBeenCalledTimes(1);
|
||||
expect(source.operation).toBe(operation);
|
||||
expect(source.cleanupPrimary).toBe(primary);
|
||||
await client.close();
|
||||
},
|
||||
);
|
||||
|
||||
describe("headless IDA producer pagination", () => {
|
||||
it("follows reported cursors for modern inventories, pseudocode, and bare-hex assembly without losing pages", async () => {
|
||||
const { producer, client } = await fixture("headless");
|
||||
|
||||
+14
-65
@@ -15,7 +15,6 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisTimeoutError,
|
||||
AnalysisResourceConstraintError,
|
||||
resourceConstraintRecovery,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -61,6 +60,7 @@ import {
|
||||
} from "../artifacts/ArtifactFileOperation.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
|
||||
|
||||
const legacyTools = [
|
||||
"get_metadata",
|
||||
@@ -489,36 +489,19 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
cause instanceof AnalysisTimeoutError &&
|
||||
cause.operation !== operation
|
||||
) {
|
||||
failure = new AnalysisTimeoutError(operation, cause.timeoutMs, {
|
||||
failure = analysisClientFailureForOperation(
|
||||
cause,
|
||||
capturedOutput: cause.capturedOutput,
|
||||
diagnostics: cause.diagnostics,
|
||||
partialObservation: cause.partialObservation,
|
||||
cleanup: cause.cleanup,
|
||||
...(cause.userMessage === undefined
|
||||
? {}
|
||||
: { userMessage: cause.userMessage }),
|
||||
...(cause.remediationAction === undefined
|
||||
? {}
|
||||
: { remediationAction: cause.remediationAction }),
|
||||
})
|
||||
.bindCleanupDependencies([cause])
|
||||
.bindCleanupResourceOwner(cause);
|
||||
"ida",
|
||||
operation,
|
||||
).bindCleanupResourceOwner(cause);
|
||||
} else if (
|
||||
cause instanceof ProviderAdapterError &&
|
||||
cause.operation === "transport"
|
||||
)
|
||||
failure = new ProviderAdapterError("ida", operation, {
|
||||
cause,
|
||||
capturedOutput: cause.capturedOutput,
|
||||
partialObservation: cause.partialObservation,
|
||||
cleanup: cause.cleanup,
|
||||
diagnostics: {
|
||||
...cause.diagnostics,
|
||||
recovery:
|
||||
"Check the configured upstream MCP registration and selected target; see docs/ida-provider.md.",
|
||||
},
|
||||
});
|
||||
failure = analysisClientFailureForOperation(cause, "ida", operation, {
|
||||
recovery:
|
||||
"Check the configured upstream MCP registration and selected target; see docs/ida-provider.md.",
|
||||
}).bindCleanupResourceOwner(cause);
|
||||
else if (cause instanceof AnalysisError) failure = cause;
|
||||
else if (cause instanceof z.ZodError)
|
||||
failure = new AnalysisOutputError(
|
||||
@@ -633,46 +616,12 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
cause.operation === "transport"
|
||||
)
|
||||
throw cause;
|
||||
const options = {
|
||||
cause,
|
||||
capturedOutput: cause.capturedOutput,
|
||||
partialObservation: cause.partialObservation,
|
||||
cleanup: cause.cleanup,
|
||||
...(cause.userMessage === undefined
|
||||
? {}
|
||||
: { userMessage: cause.userMessage }),
|
||||
diagnostics: {
|
||||
...cause.diagnostics,
|
||||
startup_operation: cause.operation,
|
||||
},
|
||||
};
|
||||
const action =
|
||||
cause.remediationAction === undefined
|
||||
? {}
|
||||
: { remediationAction: cause.remediationAction };
|
||||
// Startup is shared, but each accepted operation owns its attribution.
|
||||
// Keep the original cached failure unchanged for later callers.
|
||||
const failure =
|
||||
cause instanceof AnalysisTimeoutError
|
||||
? new AnalysisTimeoutError(operation, cause.timeoutMs, {
|
||||
...options,
|
||||
...action,
|
||||
})
|
||||
: cause instanceof AnalysisResourceConstraintError
|
||||
? new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
cause.resource,
|
||||
cause.reason,
|
||||
cause.reportedLimits,
|
||||
{ ...options, ...resourceConstraintRecovery(cause) },
|
||||
)
|
||||
: new ProviderAdapterError(cause.providerId, operation, {
|
||||
...options,
|
||||
...action,
|
||||
});
|
||||
throw failure
|
||||
.bindCleanupDependencies([cause])
|
||||
.bindCleanupResourceOwner(cause);
|
||||
// The shared boundary preserves typed outcome and cleanup facts without
|
||||
// mutating the cached startup failure.
|
||||
throw analysisClientFailureForOperation(cause, "ida", operation, {
|
||||
startup_operation: cause.operation,
|
||||
}).bindCleanupResourceOwner(cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user