mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
fix(views)!: distinguish unavailable artifact collections (#9893)
* fix(views)!: distinguish unavailable artifact collections Keep missing BEAM tables and unavailable Go and Rust build metadata separate from decoded empty collections. Retain producer limitations on model metadata, tensor, and type pages. BREAKING CHANGE: BEAM, Go, and Rust analysis view pages for unavailable collections now report coverage.status "unavailable", coverage.total null, and coverage.exhausted false. Refresh saved result schemas and handle unknown totals before using coverage to infer absence. * test(views): expect unavailable Go dependency pages Missing Go build metadata is unavailable coverage, so the MCP page reports a null total instead of an empty collection. * test(fixtures): ignore a torn build-computer note line The cancellation test reads the helper's note file while the append is still in progress. An empty file or an unfinished last line is not a record yet. A completed line still has to parse. * fix(fixtures): reject terminated malformed build-computer notes Trim removed the final newline, so a completed corrupt record looked like a torn append and the preceding notes were returned. Keep the terminator and parse every finished line. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
17b91a9853
commit
bb200646e5
11 files changed
+296
-13
No files matched your search
@@ -0,0 +1,20 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { parseAgentEnvNotes } from "../../../tests/fixtures/fakeAgentEnvComputer.js";
|
||||
|
||||
const record = { id: 1, kind: "call" };
|
||||
|
||||
it("keeps complete note records and an unfinished trailing fragment", () => {
|
||||
expect(parseAgentEnvNotes(`${JSON.stringify(record)}\n{"id":`)).toEqual([
|
||||
record,
|
||||
]);
|
||||
expect(parseAgentEnvNotes(`${JSON.stringify(record)}\n`)).toEqual([record]);
|
||||
expect(parseAgentEnvNotes("")).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects a newline-terminated note that is not a record", () => {
|
||||
expect(() =>
|
||||
parseAgentEnvNotes(`${JSON.stringify(record)}\n{bad}\n`),
|
||||
).toThrow(SyntaxError);
|
||||
expect(() => parseAgentEnvNotes("{bad}\n")).toThrow(SyntaxError);
|
||||
});
|
||||
@@ -559,6 +559,7 @@ export const erlangBeamSummaryViewResultSchema = z.strictObject({
|
||||
});
|
||||
export const erlangBeamTableViewResultSchema = z.strictObject({
|
||||
...viewResultBase,
|
||||
coverage: nullableCollectionViewCoverageSchema,
|
||||
kind: z.literal("beam"),
|
||||
parent_operation: z.literal("inspect_erlang_beam"),
|
||||
view: erlangBeamViewRequestSchema,
|
||||
@@ -624,6 +625,7 @@ export const rustBinarySummaryViewResultSchema = z.strictObject({
|
||||
|
||||
export const rustBinaryPageViewResultSchema = z.strictObject({
|
||||
...viewResultBase,
|
||||
coverage: nullableCollectionViewCoverageSchema,
|
||||
...rustBinaryViewContextSchema.shape,
|
||||
kind: z.literal("rust"),
|
||||
parent_operation: z.literal("inspect_rust_binary"),
|
||||
@@ -1506,6 +1508,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
...viewResultBase,
|
||||
...goBinaryViewContextSchema.shape,
|
||||
coverage: nullableCollectionViewCoverageSchema,
|
||||
kind: z.literal("go"),
|
||||
parent_operation: z.literal("inspect_go_binary"),
|
||||
view: goBinaryViewRequestSchema,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createEvidence } from "../evidence.js";
|
||||
import type { ErlangBeam } from "../erlang/beam.js";
|
||||
import type { ModelArtifact } from "../model/modelArtifact.js";
|
||||
import {
|
||||
projectAnalysisView,
|
||||
type AnalysisViewParent,
|
||||
} from "./analysisView.js";
|
||||
|
||||
const parent = (
|
||||
operation: string,
|
||||
result: ErlangBeam | ModelArtifact,
|
||||
): AnalysisViewParent => {
|
||||
const evidence = createEvidence(
|
||||
{ path: result.artifact.path, sha256: "a".repeat(64), format: "file" },
|
||||
{ id: "fixture", name: "Fixture reader", version: "1" },
|
||||
{ operation, parameters: {}, result },
|
||||
);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation,
|
||||
subject: evidence.subject,
|
||||
normalizedResult: result,
|
||||
limitations: [],
|
||||
};
|
||||
};
|
||||
|
||||
const beam: ErlangBeam = {
|
||||
artifact: {
|
||||
path: "/artifacts/module.beam",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 12,
|
||||
},
|
||||
form: { location: { offset: 0, bytes: 12 }, size: 4 },
|
||||
chunks: [],
|
||||
atoms: null,
|
||||
atom_encoding: null,
|
||||
exports: null,
|
||||
imports: null,
|
||||
lambdas: null,
|
||||
code: null,
|
||||
module: null,
|
||||
coverage: { status: "complete", examined_chunks: 0, atoms: 0 },
|
||||
limitations: [],
|
||||
};
|
||||
|
||||
it.each(["atoms", "exports", "imports", "lambdas"] as const)(
|
||||
"distinguishes unavailable BEAM %s from a decoded empty table",
|
||||
(collection) => {
|
||||
for (const available of [false, true]) {
|
||||
const result = projectAnalysisView(
|
||||
parent("inspect_erlang_beam", {
|
||||
...beam,
|
||||
[collection]: available ? [] : null,
|
||||
}),
|
||||
{ kind: "beam", collection, offset: 7, limit: 1 },
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.coverage).toEqual({
|
||||
status: available ? "empty" : "unavailable",
|
||||
examined: 0,
|
||||
total: available ? 0 : null,
|
||||
next_offset: null,
|
||||
exhausted: available,
|
||||
});
|
||||
expect(result.value).toMatchObject({ page: { collection, items: [] } });
|
||||
if (!available)
|
||||
expect(result.value.unknowns.join(" ")).toContain(
|
||||
`No ${collection} table was decoded`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const model: ModelArtifact = {
|
||||
artifact: {
|
||||
path: "/artifacts/model.gguf",
|
||||
file_size: 32,
|
||||
header_sha256: "a".repeat(64),
|
||||
header_length: 32,
|
||||
sha256: "a".repeat(64),
|
||||
bytes_read: 32,
|
||||
read_scope: "full-file",
|
||||
},
|
||||
format: "gguf",
|
||||
version: 3,
|
||||
byte_order: "little",
|
||||
alignment: 32,
|
||||
tensor_data_offset: 32,
|
||||
metadata: [],
|
||||
tensors: [],
|
||||
totals: {
|
||||
confidence: "derived",
|
||||
parameter_count: "0",
|
||||
tensor_bytes: 0,
|
||||
by_type: [],
|
||||
},
|
||||
coverage: {
|
||||
header: "complete",
|
||||
tensor_data: "not-interpreted",
|
||||
array_values: "preview",
|
||||
tensor_ranges: "partial",
|
||||
storage_sizes: "partial",
|
||||
},
|
||||
limitations: [
|
||||
"Array metadata retains only the requested preview; storage sizes remain unknown.",
|
||||
],
|
||||
};
|
||||
|
||||
it.each(["metadata", "tensors", "types"] as const)(
|
||||
"retains producer limitations on model %s pages with no envelope warnings",
|
||||
(collection) => {
|
||||
const input = parent("inspect_model_artifact", model);
|
||||
const result = projectAnalysisView(input, {
|
||||
kind: "model-artifact",
|
||||
collection,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.limitations).toEqual(model.limitations);
|
||||
expect(input.limitations).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
unavailableViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { selectViewRows } from "./selectViewRows.js";
|
||||
|
||||
@@ -120,7 +121,7 @@ type Row = NonNullable<ErlangBeam[Table]>[number];
|
||||
type BeamView = z.infer<typeof erlangBeamViewRequestSchema>;
|
||||
|
||||
/** Select already parsed rows; original atom/lambda indexes are never repurposed. */
|
||||
const selectRows = (rows: readonly Row[] | null, view: BeamView) => {
|
||||
const selectRows = (source: readonly Row[], view: BeamView) => {
|
||||
const names = view.names === undefined ? undefined : new Set(view.names);
|
||||
const indices =
|
||||
view.indices === undefined ? undefined : new Set(view.indices);
|
||||
@@ -138,7 +139,6 @@ const selectRows = (rows: readonly Row[] | null, view: BeamView) => {
|
||||
),
|
||||
source_index,
|
||||
});
|
||||
const source = rows ?? [];
|
||||
if (names === undefined && indices === undefined && modules === undefined)
|
||||
return {
|
||||
rows: source
|
||||
@@ -223,7 +223,17 @@ export const projectErlangBeamView = (
|
||||
summary,
|
||||
coverage: completeWithinViewCoverage(chunks.length, chunks.length),
|
||||
});
|
||||
const selected = selectRows(parsed.value[view.collection], view);
|
||||
const rows = parsed.value[view.collection];
|
||||
if (rows === null)
|
||||
return ok({
|
||||
...shared,
|
||||
kind: "beam" as const,
|
||||
view,
|
||||
beam: summary,
|
||||
page: { collection: view.collection, items: [] },
|
||||
coverage: unavailableViewCoverage(),
|
||||
});
|
||||
const selected = selectRows(rows, view);
|
||||
return ok({
|
||||
...shared,
|
||||
kind: "beam" as const,
|
||||
|
||||
@@ -238,3 +238,53 @@ it("refuses string_literals view when unrequested and projects filtered/truncate
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["dependencies", "settings"] as const)(
|
||||
"distinguishes unavailable Go %s from decoded empty records",
|
||||
(collection) => {
|
||||
for (const available of [false, true]) {
|
||||
const input = parent();
|
||||
const result = projectAnalysisView(
|
||||
{
|
||||
...input,
|
||||
normalizedResult: {
|
||||
...report,
|
||||
build_info: available
|
||||
? {
|
||||
header_offset: 0,
|
||||
encoding: "inline",
|
||||
go_version: null,
|
||||
go_version_bytes_base64: "",
|
||||
module_text: null,
|
||||
module_bytes_base64: "",
|
||||
version_location: { offset: 32, bytes: 0 },
|
||||
module_location: { offset: 32, bytes: 0 },
|
||||
module: {
|
||||
path: null,
|
||||
main: null,
|
||||
dependencies: [],
|
||||
settings: [],
|
||||
unparsed_lines: [],
|
||||
unparsed_line_bytes_base64: [],
|
||||
complete: true,
|
||||
},
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
{ kind: "go", collection, offset: 3, limit: 1 },
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.coverage).toEqual({
|
||||
status: available ? "empty" : "unavailable",
|
||||
examined: 0,
|
||||
total: available ? 0 : null,
|
||||
next_offset: null,
|
||||
exhausted: available,
|
||||
});
|
||||
expect(result.value).toMatchObject({
|
||||
source_coverage: { build_info_available: available },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
unavailableViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
@@ -322,10 +323,18 @@ export const projectGoBinaryView = (
|
||||
),
|
||||
});
|
||||
}
|
||||
if (info === null)
|
||||
return ok({
|
||||
kind: "go" as const,
|
||||
view,
|
||||
...shared,
|
||||
page: { collection: view.collection, rows: [] },
|
||||
coverage: unavailableViewCoverage(),
|
||||
});
|
||||
if (view.collection === "dependencies") {
|
||||
const paths = view.paths === undefined ? undefined : new Set(view.paths);
|
||||
const selected = selectViewRows(
|
||||
info?.module.dependencies ?? [],
|
||||
info.module.dependencies,
|
||||
view,
|
||||
(row, index) =>
|
||||
(indices?.has(index) ?? true) && (paths?.has(row.path) ?? true),
|
||||
@@ -353,7 +362,7 @@ export const projectGoBinaryView = (
|
||||
}
|
||||
const keys = view.keys === undefined ? undefined : new Set(view.keys);
|
||||
const selected = selectViewRows(
|
||||
info?.module.settings ?? [],
|
||||
info.module.settings,
|
||||
view,
|
||||
(row, index) =>
|
||||
(indices?.has(index) ?? true) && (keys?.has(row.key) ?? true),
|
||||
|
||||
@@ -217,7 +217,7 @@ export const projectModelArtifactView = (
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
|
||||
limitations: boundedViewMessages(
|
||||
parent.limitations,
|
||||
[...parent.limitations, ...report.limitations],
|
||||
parent.evidenceId,
|
||||
"limitations",
|
||||
),
|
||||
|
||||
@@ -206,3 +206,40 @@ it("pages matching original indices and omits dependency vectors unless selected
|
||||
next_offset: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["packages", "edges"] as const)(
|
||||
"distinguishes unavailable Rust %s from decoded empty records",
|
||||
(collection) => {
|
||||
for (const available of [false, true]) {
|
||||
const metadata = report.auditable_metadata!;
|
||||
const result = projectAnalysisView(
|
||||
parent({
|
||||
...report,
|
||||
auditable_metadata: available
|
||||
? {
|
||||
...metadata,
|
||||
packages: [],
|
||||
root_package_index: null,
|
||||
dependency_edges: [],
|
||||
cyclic_dependency_indices: [],
|
||||
}
|
||||
: null,
|
||||
}),
|
||||
{ kind: "rust", collection, offset: 3, limit: 1 },
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.coverage).toEqual({
|
||||
status: available ? "empty" : "unavailable",
|
||||
examined: 0,
|
||||
total: available ? 0 : null,
|
||||
next_offset: null,
|
||||
exhausted: available,
|
||||
});
|
||||
expect(result.value).toMatchObject({
|
||||
source_coverage: {
|
||||
embedded_records: available ? "complete" : "unknown",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
unavailableViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { selectViewRows } from "./selectViewRows.js";
|
||||
@@ -190,6 +191,15 @@ export const projectRustBinaryView = (
|
||||
summary,
|
||||
coverage: completeWithinViewCoverage(1, 1),
|
||||
});
|
||||
if (metadata === null)
|
||||
return ok({
|
||||
kind: "rust" as const,
|
||||
view,
|
||||
...shared,
|
||||
rust: summary,
|
||||
page: { collection: view.collection, rows: [] },
|
||||
coverage: unavailableViewCoverage(),
|
||||
});
|
||||
const indices =
|
||||
view.indices === undefined ? undefined : new Set(view.indices);
|
||||
const fields =
|
||||
@@ -199,7 +209,7 @@ export const projectRustBinaryView = (
|
||||
const versions =
|
||||
view.versions === undefined ? undefined : new Set(view.versions);
|
||||
const selected = selectViewRows(
|
||||
metadata?.packages ?? [],
|
||||
metadata.packages,
|
||||
view,
|
||||
(row, index) =>
|
||||
(indices?.has(index) ?? true) &&
|
||||
@@ -238,7 +248,7 @@ export const projectRustBinaryView = (
|
||||
const to =
|
||||
view.to_indices === undefined ? undefined : new Set(view.to_indices);
|
||||
const selected = selectViewRows(
|
||||
metadata?.dependency_edges ?? [],
|
||||
metadata.dependency_edges,
|
||||
view,
|
||||
(row, index) =>
|
||||
(indices?.has(index) ?? true) &&
|
||||
|
||||
@@ -368,7 +368,7 @@ mcpTest(
|
||||
const page = goPage(parseEvidence(empty.structuredContent));
|
||||
expect(page).toMatchObject({
|
||||
page: { rows: [] },
|
||||
coverage: { total: 0 },
|
||||
coverage: { status: "unavailable", total: null, exhausted: false },
|
||||
source_coverage: {
|
||||
build_info_available: false,
|
||||
module_parse_complete: null,
|
||||
|
||||
+23
-4
@@ -732,6 +732,28 @@ export interface FakeAgentEnvRunner {
|
||||
notes(name: string): Promise<unknown[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSONL notes from the fake build computer. A trailing fragment without a
|
||||
* newline is still being appended. Every newline-terminated line is a record
|
||||
* and must parse.
|
||||
*/
|
||||
export const parseAgentEnvNotes = (text: string): unknown[] => {
|
||||
if (text.length === 0) return [];
|
||||
const terminated = text.endsWith("\n");
|
||||
const lines = text.split("\n");
|
||||
if (terminated) lines.pop();
|
||||
const records: unknown[] = [];
|
||||
for (const [index, line] of lines.entries()) {
|
||||
try {
|
||||
records.push(JSON.parse(line) as unknown);
|
||||
} catch (error) {
|
||||
if (!terminated && index === lines.length - 1) break;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return records;
|
||||
};
|
||||
|
||||
export const fakeAgentEnvRunner = async (
|
||||
computer: Pick<FakeComputer, "url" | "token">,
|
||||
): Promise<FakeAgentEnvRunner> => {
|
||||
@@ -769,10 +791,7 @@ export const fakeAgentEnvRunner = async (
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return (await readFile(path, "utf8"))
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as unknown);
|
||||
return parseAgentEnvNotes(await readFile(path, "utf8"));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user