fix(firmware): derive extraction failure from producer severity

This commit is contained in:
morluto committed 2026-10-24 02:29:08 +08:00
1 parent a732bd0656
commit 243790adc9
5 files changed
+198 -7

No files matched your search

+5
View File
@@ -139,6 +139,11 @@ build's source revision, so that field remains unknown.
share recovery prose without losing any region identity. Instructions use
already configured capabilities where one exists. Every extraction rerun
requires a fresh absent `output_directory`, preserving the prior evidence.
Unblob's declared `ERROR` severity establishes an extraction failure,
including standalone `UnknownError` task exceptions. Warnings and resolved
extraction problems remain diagnostics with partial coverage; they do not
establish failed extractor stages. Standalone errors do not acquire a nearby
chunk's handler or byte range.
Nested unknown chunks use `inspect_firmware_regions` on the actual published
child rather than repeating the same extraction with unchanged handlers.
Inspection requires separately configured Binwalk (`REA_BINWALK_COMMAND`);
+112 -2
View File
@@ -382,6 +382,116 @@ describe("firmware producer chunk failures", () => {
});
});
describe("firmware producer error severity", () => {
const outcomes = [
{
name: "task exception",
report: {
__typename__: "UnknownError",
severity: "ERROR",
exception: "inert fixture",
},
failed: true,
},
{
name: "future declared error",
report: { __typename__: "FutureProducerError", severity: "ERROR" },
failed: true,
},
{
name: "command warning",
report: {
__typename__: "ExtractCommandFailedReport",
severity: "WARNING",
command: "fixture",
stdout: "",
stderr: "",
exit_code: 1,
},
failed: false,
},
{
name: "resolved extraction problem",
report: {
__typename__: "ExtractionProblem",
problem: "duplicate path",
resolution: "kept first entry",
path: "config",
},
failed: false,
},
];
it.each(["task", "chunk", "multi-file"] as const)(
"preserves diagnostic outcomes at the %s owner",
(owner) => {
for (const { report, failed } of outcomes) {
const record = root();
const observation =
owner === "task"
? report
: owner === "chunk"
? {
__typename__: "ChunkReport",
id: "stage-id",
handler_name: "gzip",
start_offset: 0,
end_offset: 20,
size: 20,
is_encrypted: false,
extraction_reports: [report],
}
: {
__typename__: "MultiFileReport",
id: "stage-id",
handler_name: "gzip",
name: "rootfs",
paths: [],
extraction_reports: [report],
};
const normalized = normalizeUnblobReport(
[
{
...record,
reports: [
...record.reports.filter(
(item) => item.__typename__ !== "UnknownChunkReport",
),
observation,
],
},
],
context,
);
expect(normalized.coverage).toBe("partial");
expect(normalized.diagnostics).toHaveLength(1);
expect(normalized.diagnostics[0]).toMatchObject({
input_path: "$input",
report: owner === "multi-file" ? observation : report,
});
if (owner === "chunk") {
expect(normalized.chunks[0]?.extraction_failed).toBe(failed);
expect(normalized.failed_extraction_stages).toEqual([]);
} else {
expect(normalized.failed_extraction_stages).toEqual(
failed
? [
{
input_path: "$input",
provider_id: owner === "task" ? null : "stage-id",
handler: owner === "task" ? null : "gzip",
dependencies: [],
kind: "extractor-failure",
},
]
: [],
);
}
}
},
);
});
describe("firmware task dependency failures", () => {
it.each([{ dependencies: [] }, { dependencies: ["", ""] }])(
"retains unnamed dependency failures ($dependencies)",
@@ -686,9 +796,9 @@ describe("failed multi-file extraction stages", () => {
is_multi_file: true,
};
const stageFailure = {
__typename__: "ExtractorExecutionErrorReport",
__typename__: "UnknownError",
severity: "ERROR",
handler: "multipart-firmware",
exception: "inert multi-file extraction failure",
};
const normalized = normalizeUnblobReport(
[
+14 -2
View File
@@ -61,6 +61,12 @@ const missingDependency = z.object({
__typename__: z.literal("ExtractorDependencyNotFoundReport"),
dependencies: z.array(z.string()),
});
// Unblob reports resolved extraction problems and warnings alongside errors.
// The declared severity owns failure; a diagnostic's presence or class name
// alone does not establish that extraction failed.
const extractionError = z.object({ severity: z.literal("ERROR") });
const hasExtractionError = (reports: readonly unknown[]): boolean =>
reports.some((item) => extractionError.safeParse(item).success);
const region = z.object({
id: z.string(),
start_offset: integer,
@@ -359,7 +365,8 @@ export const normalizeUnblobReport = (
);
noteMissingDependencies(path, [item]);
if (
item.__typename__ === "ExtractorExecutionErrorReport" &&
hasExtractionError([item]) &&
!missingDependency.safeParse(item).success &&
!taskFailures.has(path)
) {
taskFailures.add(path);
@@ -396,7 +403,7 @@ export const normalizeUnblobReport = (
.parse(item)
: null;
const extractionFailed =
detail !== null && detail.extraction_reports.length > 0;
detail !== null && hasExtractionError(detail.extraction_reports);
if (detail !== null)
for (const problem of detail.extraction_reports) {
diagnostics.push(
@@ -446,6 +453,11 @@ export const normalizeUnblobReport = (
// The report owns its failure and dependencies. A produced subtask
// is a derivation, not a new owner for the parent's report.
const dependencies = dependencyNames(parsed.extraction_reports);
if (
dependencies === null &&
!hasExtractionError(parsed.extraction_reports)
)
continue;
const rows =
dependencies === null
? [{ dependencies: [] as readonly string[] }]
@@ -524,6 +524,49 @@ it("reports a failed multi-file stage as an extractor-failure gap", async () =>
);
});
it.each(["task-error", "task-warning"])(
"preserves standalone diagnostic recovery without inventing stage ownership: %s",
async (mode) => {
const fixture = await firmwareFixture(mode);
const result = await fixture.service.execute("extract_firmware", {
path: fixture.path,
output_directory: fixture.output,
});
if (!result.ok) throw result.error;
const normalized = firmwareResultSchemas.extract_firmware.parse(
result.value.normalized_result,
);
const failures = gapsWithContinuations(normalized).filter(
(gap) => gap.kind === "extractor-failure",
);
expect(normalized.diagnostics).toContainEqual(
expect.objectContaining({
input_path: "$input",
report: expect.objectContaining({
__typename__:
mode === "task-error"
? "UnknownError"
: "ExtractCommandFailedReport",
}),
}),
);
if (mode === "task-error") {
expect(failures).toEqual([
expect.objectContaining({
input_path: "$input",
handler: null,
provider_id: null,
range: null,
root_file_range: null,
dependencies: [],
continuation: expect.stringContaining("diagnostics"),
}),
]);
} else expect(failures).toEqual([]);
await assertFirmwareCleanup(fixture.launches);
},
);
it("carries the owning multi-file stage on a task-level dependency gap", async () => {
const fixture = await firmwareFixture("multi-file-dependency");
const result = await fixture.service.execute("extract_firmware", {
+24 -3
View File
@@ -325,9 +325,9 @@ if (args.includes("--version")) {
dependencies: ["sasquatch"],
}
: {
__typename__: "ExtractorExecutionErrorReport",
__typename__: "UnknownError",
severity: "ERROR",
handler: "tar",
exception: "inert multi-file extraction failure",
},
],
},
@@ -381,6 +381,23 @@ if (args.includes("--version")) {
},
];
}
if (mode === "task-error" || mode === "task-warning")
records[1].reports.push(
mode === "task-error"
? {
__typename__: "UnknownError",
severity: "ERROR",
exception: "inert task exception",
}
: {
__typename__: "ExtractCommandFailedReport",
severity: "WARNING",
command: "fixture",
stdout: "",
stderr: "",
exit_code: 1,
},
);
if (
mode !== "depth-reentry" &&
mode !== "depth-orphan" &&
@@ -393,7 +410,11 @@ if (args.includes("--version")) {
? "["
: JSON.stringify(records),
);
if (mode === "dependency" || mode === "multi-file-dependency")
if (
mode === "dependency" ||
mode === "multi-file-dependency" ||
mode === "task-error"
)
process.exitCode = 1;
if (
mode === "workspace-cleanup-failure" ||