mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix: retain status observations when session cleanup fails
This commit is contained in:
1 parent
aedd394471
commit
37122745b6
3 files changed
+135
-25
No files matched your search
@@ -1,4 +1,3 @@
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -33,7 +32,7 @@ export const withSessionCleanup = async <Value>(
|
||||
|
||||
/** Project typed one-shot outcomes only after execution and cleanup settle. */
|
||||
export const directAnalysisCleanupFailure = (
|
||||
result: Result<Evidence, AnalysisError>,
|
||||
result: Result<JsonValue, AnalysisError>,
|
||||
error: AnalysisError,
|
||||
): JsonValue => {
|
||||
const cleanup = projectAnalysisError(error);
|
||||
|
||||
@@ -10,6 +10,11 @@ import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
|
||||
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
|
||||
import { createCatalogIdentity } from "../catalogIdentity.js";
|
||||
import { compactPresentationForMcpInputSchemaProfile } from "../config/mcpInputSchemaProfile.js";
|
||||
import {
|
||||
directAnalysisCleanupFailure,
|
||||
withSessionCleanup,
|
||||
} from "./DirectAnalysisCleanup.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
|
||||
/** Read target-free provider status and operation availability. */
|
||||
export const runSessionStatus = async (
|
||||
@@ -22,28 +27,30 @@ export const runSessionStatus = async (
|
||||
const config = dependencies.readConfiguration();
|
||||
if (!config.ok) return { error: projectAnalysisError(config.error) };
|
||||
const session = await dependencies.createBinarySession(config.value, logger);
|
||||
try {
|
||||
const status = jsonObjectSchema.parse(await session.status());
|
||||
return {
|
||||
...status,
|
||||
capabilities: [
|
||||
...providerCapabilitySet.array().parse(status.capabilities),
|
||||
...providerCapabilitySets([
|
||||
sqliteDatabaseCapability(process.versions.node),
|
||||
]),
|
||||
],
|
||||
server_identity: createServerIdentity({
|
||||
startedAt: new Date().toISOString(),
|
||||
commandPath: CURRENT_ENTRYPOINT_PATH,
|
||||
catalog: createCatalogIdentity(
|
||||
compactPresentationForMcpInputSchemaProfile(
|
||||
config.value.mcpInputSchemaProfile,
|
||||
return withSessionCleanup<JsonValue>(
|
||||
session,
|
||||
async () => {
|
||||
const status = jsonObjectSchema.parse(await session.status());
|
||||
return {
|
||||
...status,
|
||||
capabilities: [
|
||||
...providerCapabilitySet.array().parse(status.capabilities),
|
||||
...providerCapabilitySets([
|
||||
sqliteDatabaseCapability(process.versions.node),
|
||||
]),
|
||||
],
|
||||
server_identity: createServerIdentity({
|
||||
startedAt: new Date().toISOString(),
|
||||
commandPath: CURRENT_ENTRYPOINT_PATH,
|
||||
catalog: createCatalogIdentity(
|
||||
compactPresentationForMcpInputSchemaProfile(
|
||||
config.value.mcpInputSchemaProfile,
|
||||
),
|
||||
config.value.mcpOutputSchemaProfile ?? "full",
|
||||
),
|
||||
config.value.mcpOutputSchemaProfile ?? "full",
|
||||
),
|
||||
}),
|
||||
};
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}),
|
||||
};
|
||||
},
|
||||
(status, cleanup) => directAnalysisCleanupFailure(ok(status), cleanup),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Cli } from "incur";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { runSessionStatus } from "../../../src/application/DirectAnalysisStatus.js";
|
||||
import { registerUtilityCommands } from "../../../src/cli/utilityCommands.js";
|
||||
import { parseConfig } from "../../../src/config/parseConfig.js";
|
||||
import { isCliOperationFailure } from "../../../src/cliLogging.js";
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { observed } from "../../fixtures/analysisExecution.js";
|
||||
|
||||
const createStatusSession = () =>
|
||||
createTestBinarySession(() => ({
|
||||
close: async () => ok(null),
|
||||
execute: async () => observed(null),
|
||||
}));
|
||||
|
||||
it.each(["capabilities", "providers"])(
|
||||
"reports failed %s finalization with the completed status",
|
||||
async (command) => {
|
||||
const session = createStatusSession();
|
||||
const cleanup = new ProviderCleanupError(
|
||||
"fixture",
|
||||
["retained profile reader"],
|
||||
{ reason: "profile reader close failed" },
|
||||
{ remediationAction: "Retry the retained profile reader cleanup." },
|
||||
);
|
||||
const close = vi
|
||||
.spyOn(session, "close")
|
||||
.mockResolvedValueOnce(err(cleanup));
|
||||
const cli = Cli.create({ name: "rea" });
|
||||
registerUtilityCommands(
|
||||
cli,
|
||||
silentLogger,
|
||||
{},
|
||||
{
|
||||
runSessionStatus: () =>
|
||||
runSessionStatus({
|
||||
readConfiguration: () => parseConfig({}),
|
||||
createBinarySession: () => session,
|
||||
}),
|
||||
runProviderAnalysis: async () => null,
|
||||
},
|
||||
);
|
||||
let stdout = "";
|
||||
try {
|
||||
await cli.serve([command, "--json"], {
|
||||
env: {},
|
||||
stdout: (text) => {
|
||||
stdout += text;
|
||||
},
|
||||
exit: () => {},
|
||||
});
|
||||
const output: unknown = JSON.parse(stdout);
|
||||
expect(output).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
remediation: { action: "Retry the retained profile reader cleanup." },
|
||||
details: {
|
||||
resources: ["retained profile reader"],
|
||||
partial_observation: {
|
||||
capabilities: expect.any(Array),
|
||||
server_identity: expect.any(Object),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(isCliOperationFailure(output)).toBe(true);
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
close.mockRestore();
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps status failure when final cleanup also fails: %s",
|
||||
async (cleanupFails) => {
|
||||
const session = createStatusSession();
|
||||
const failure = new Error("status observation failed");
|
||||
const cleanup = new ProviderCleanupError("fixture", ["reader"], {});
|
||||
const status = vi.spyOn(session, "status").mockRejectedValueOnce(failure);
|
||||
const close = vi
|
||||
.spyOn(session, "close")
|
||||
.mockResolvedValueOnce(cleanupFails ? err(cleanup) : ok(null));
|
||||
try {
|
||||
const result = runSessionStatus({
|
||||
readConfiguration: () => parseConfig({}),
|
||||
createBinarySession: () => session,
|
||||
});
|
||||
if (cleanupFails)
|
||||
await expect(result).rejects.toMatchObject({
|
||||
cause: failure,
|
||||
errors: [failure, cleanup],
|
||||
});
|
||||
else await expect(result).rejects.toBe(failure);
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
status.mockRestore();
|
||||
close.mockRestore();
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user