mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(environment)!: attribute source errors to caller fields (#7900)
* fix(environment): attribute source file errors to caller fields * style(environment): format source admission tests
This commit is contained in:
1 parent
c7722c8eeb
commit
5147b6e306
6 files changed
+73
-13
No files matched your search
@@ -33,6 +33,7 @@ const captureFile = async (
|
||||
"evidence",
|
||||
source.path,
|
||||
EXPORT_ENVIRONMENT_BUNDLE_OPERATION,
|
||||
["source", "path"],
|
||||
);
|
||||
const retained = resolveEvidenceInput(source, lookup);
|
||||
if (!retained.ok) return retained;
|
||||
|
||||
@@ -243,7 +243,7 @@ export const environmentExportEvidenceFile = async (
|
||||
const refused = headroomRefusal(operation, path, size);
|
||||
if (refused !== undefined) throw refused;
|
||||
});
|
||||
return await environmentExportFile(role, path, bytes, operation);
|
||||
return await environmentExportFile(role, path, bytes, { operation });
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof AnalysisResourceConstraintError) return err(cause);
|
||||
throw cause;
|
||||
@@ -255,8 +255,12 @@ export const environmentExportFile = async (
|
||||
role: "manifest" | "evidence",
|
||||
path: string,
|
||||
bytes: Buffer,
|
||||
operation: string = ENVIRONMENT_EXPORT_OPERATION,
|
||||
context: {
|
||||
operation?: string;
|
||||
fieldPath?: AnalysisInputIssue["path"] | undefined;
|
||||
} = {},
|
||||
): Promise<Result<EnvironmentExportFile, AnalysisError>> => {
|
||||
const operation = context.operation ?? ENVIRONMENT_EXPORT_OPERATION;
|
||||
const refusal = headroomRefusal(operation, path, bytes.length);
|
||||
if (refusal !== undefined) return err(refusal);
|
||||
let parsed: ReturnType<typeof parseUtf8Json>;
|
||||
@@ -271,7 +275,7 @@ export const environmentExportFile = async (
|
||||
inputError(
|
||||
operation,
|
||||
{
|
||||
path: [role],
|
||||
path: context.fieldPath ?? [role],
|
||||
reason: "invalid_format",
|
||||
expected: "JSON",
|
||||
message: parsed.error,
|
||||
@@ -289,6 +293,7 @@ export const readEnvironmentExportFile = async (
|
||||
role: "manifest" | "evidence",
|
||||
path: string,
|
||||
operation: string = ENVIRONMENT_EXPORT_OPERATION,
|
||||
fieldPath?: AnalysisInputIssue["path"],
|
||||
): Promise<Result<EnvironmentExportFile, AnalysisError>> => {
|
||||
const resources = new ArtifactResourceScope();
|
||||
let bytes: Buffer;
|
||||
@@ -336,7 +341,7 @@ export const readEnvironmentExportFile = async (
|
||||
inputError(
|
||||
operation,
|
||||
{
|
||||
path: [role],
|
||||
path: fieldPath ?? [role],
|
||||
reason: "invalid_value",
|
||||
expected: "one stable regular file snapshot",
|
||||
message: `The ${role} file changed while it was read.`,
|
||||
@@ -349,9 +354,12 @@ export const readEnvironmentExportFile = async (
|
||||
inputError(
|
||||
operation,
|
||||
{
|
||||
path: [role],
|
||||
path: fieldPath ?? [role],
|
||||
reason: "invalid_value",
|
||||
message: `The ${role} argument must be a readable file path; inline JSON cannot be exported.`,
|
||||
message:
|
||||
fieldPath === undefined
|
||||
? `The ${role} argument must be a readable file path; inline JSON cannot be exported.`
|
||||
: "The selected source path must be a readable regular file.",
|
||||
},
|
||||
`${path} could not be read as the ${role} file.`,
|
||||
),
|
||||
@@ -360,7 +368,7 @@ export const readEnvironmentExportFile = async (
|
||||
if (denied !== undefined) return err(denied);
|
||||
throw cause;
|
||||
}
|
||||
return environmentExportFile(role, path, bytes, operation);
|
||||
return environmentExportFile(role, path, bytes, { operation, fieldPath });
|
||||
};
|
||||
|
||||
/** Read both files once; the caller authenticates the parsed values and the export copies the same bytes. */
|
||||
|
||||
@@ -429,6 +429,7 @@ const buildEnvironmentCommand = async (
|
||||
"evidence",
|
||||
capturePath,
|
||||
ENVIRONMENT_BUILD_OPERATION,
|
||||
["capture"],
|
||||
);
|
||||
if (!read.ok) return cliError(read.error);
|
||||
capture = read.value;
|
||||
|
||||
@@ -25,6 +25,25 @@ const listFiles = async (root: string): Promise<string[]> =>
|
||||
)
|
||||
.sort();
|
||||
|
||||
cliTest(
|
||||
"attributes invalid capture JSON to the capture argument",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-environment-invalid-capture-",
|
||||
);
|
||||
const capture = join(root, "invalid.json");
|
||||
await writeFile(capture, "{");
|
||||
const result = await cli.run({
|
||||
arguments: ["environment-build", capture, join(root, "output"), "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: { issues: [{ path: ["capture"], reason: "invalid_format" }] },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"builds the bundle the three CLI steps build, byte for byte",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -202,6 +202,40 @@ it("copies a capture Evidence file byte for byte and plans without writing on a
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("attributes source file admission and JSON failures to source.path", async () => {
|
||||
const { client } = await connect();
|
||||
const root = await createTestTempDirectory("rea-bundle-source-errors-");
|
||||
await mkdir(join(root, "directory"));
|
||||
await writeFile(join(root, "invalid-utf8"), Buffer.from([0xff]));
|
||||
await writeFile(join(root, "invalid-json"), "{");
|
||||
for (const [name, reason] of [
|
||||
["missing", "invalid_value"],
|
||||
["directory", "invalid_value"],
|
||||
["invalid-utf8", "invalid_format"],
|
||||
["invalid-json", "invalid_format"],
|
||||
] as const) {
|
||||
const output = join(root, `out-${name}`);
|
||||
const result = await client.callTool({
|
||||
name: TOOL,
|
||||
arguments: {
|
||||
source: { kind: "evidence-file", path: join(root, name) },
|
||||
output_directory: output,
|
||||
dry_run: true,
|
||||
},
|
||||
});
|
||||
const { error } = parseMcpToolError(result);
|
||||
expect(error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
recovery: { kind: "inspect_input_contract", operation: TOOL },
|
||||
details: { issues: [{ path: ["source", "path"], reason }] },
|
||||
});
|
||||
expect(JSON.stringify(error)).not.toContain(
|
||||
"inline JSON cannot be exported",
|
||||
);
|
||||
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses an existing or parentless output directory and leaves it as it was", async () => {
|
||||
const { client, session } = await connect();
|
||||
const capture = analysisViewCaptureEvidence(
|
||||
|
||||
@@ -37,12 +37,9 @@ it.each(["serialize", "parse"] as const)(
|
||||
capture,
|
||||
"environment-build",
|
||||
)
|
||||
: await environmentExportFile(
|
||||
"evidence",
|
||||
"capture.json",
|
||||
bytes,
|
||||
"environment-build",
|
||||
);
|
||||
: await environmentExportFile("evidence", "capture.json", bytes, {
|
||||
operation: "environment-build",
|
||||
});
|
||||
if (result.ok) throw new Error("Expected a memory refusal");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
|
||||
Reference in new issue
Block a user