fix(firmware): retain independent failures at each extraction owner

This commit is contained in:
morluto committed 2026-10-24 02:39:36 +08:00
1 parent 115a78fe24
commit 7298bd08d1
6 files changed
+186 -47

No files matched your search

+1 -1
View File
@@ -108,7 +108,7 @@ const chunk = z.strictObject({
handler: z.string().nullable(),
encrypted: z.boolean().nullable(),
root_file_range: range.nullable(),
/** The producer reported extraction problems for this chunk. */
/** The producer reported an extraction error or missing dependency for this chunk. */
extraction_failed: z.boolean(),
});
/** One unextracted region or failing task/stage, with a supported continuation
+8 -19
View File
@@ -178,7 +178,12 @@ export const publishFirmwareExtraction = async (context: {
unpublished.length > 0 ? "partial" : context.normalized.coverage,
files,
unpublished_entries: unpublished,
chunks: context.normalized.chunks,
chunks: context.normalized.chunks.map(
({ extraction_failure_kinds, ...chunk }) => ({
...chunk,
extraction_failed: extraction_failure_kinds.length > 0,
}),
),
derivations: context.normalized.derivations,
depth_limited_paths: context.normalized.depth_limited_paths,
diagnostics: context.normalized.diagnostics,
@@ -390,13 +395,6 @@ const extractionGaps = (
? nestedContinuationId(chunk.input_path)
: continuation(GAP_CONTINUATIONS[kind]),
}));
// Chunks whose failure a dependency row already covers stay out of the
// generic extractor-failure emission.
const dependencyChunkKeys = new Set(
normalized.chunk_missing_dependencies.map(({ input_path, provider_id }) =>
JSON.stringify([input_path, provider_id]),
),
);
// A depth ceiling can only be re-rooted through a published child that a
// fresh firmware input could actually accept; the index is only needed
// when a depth row will actually consult it.
@@ -451,17 +449,8 @@ const extractionGaps = (
})),
...chunkGaps("unknown-chunk-handler", (chunk) => chunk.handler === null),
...chunkGaps("encrypted-chunk", (chunk) => chunk.encrypted === true),
...chunkGaps(
"extractor-failure",
(chunk) =>
chunk.extraction_failed === true &&
chunk.handler !== null &&
chunk.encrypted !== true &&
// The chunk's own dependency row already carries its recovery; a
// task-wide dependency names no region and replaces nothing.
!dependencyChunkKeys.has(
JSON.stringify([chunk.input_path, chunk.provider_id]),
),
...chunkGaps("extractor-failure", (chunk) =>
chunk.extraction_failure_kinds.includes("extractor-failure"),
),
...normalized.depth_limited_paths.map((input_path): ExtractionGap => ({
kind: "depth-limited",
+87 -2
View File
@@ -376,13 +376,96 @@ describe("firmware producer chunk failures", () => {
expect(normalized.chunks).toHaveLength(1);
expect(normalized.chunks[0]).toMatchObject({
handler: "zip",
extraction_failed: true,
extraction_failure_kinds: ["extractor-failure"],
});
expect(normalized.coverage).toBe("partial");
});
});
describe("firmware producer error severity", () => {
it.each(["chunk", "multi-file"] as const)(
"retains independent failure kinds at one %s owner",
(owner) => {
const record = root();
const extraction_reports = [
{
__typename__: "ExtractorDependencyNotFoundReport",
severity: "ERROR",
dependencies: ["sasquatch"],
},
{
__typename__: "UnknownError",
severity: "ERROR",
exception: "independent failure",
},
];
const observation =
owner === "chunk"
? {
__typename__: "ChunkReport",
id: "stage-id",
handler_name: "gzip",
start_offset: 0,
end_offset: 20,
size: 20,
is_encrypted: false,
extraction_reports,
}
: {
__typename__: "MultiFileReport",
id: "stage-id",
handler_name: "gzip",
name: "rootfs",
paths: [],
extraction_reports,
};
const normalized = normalizeUnblobReport(
[
{
...record,
reports: [
...record.reports.filter(
(item) => item.__typename__ !== "UnknownChunkReport",
),
observation,
],
},
],
context,
);
if (owner === "chunk") {
expect(normalized.chunks[0]?.extraction_failure_kinds).toEqual([
"missing-extractor-dependency",
"extractor-failure",
]);
expect(normalized.chunk_missing_dependencies).toEqual([
{
input_path: "$input",
provider_id: "stage-id",
dependencies: ["sasquatch"],
},
]);
} else {
expect(normalized.failed_extraction_stages).toEqual([
{
input_path: "$input",
provider_id: "stage-id",
handler: "gzip",
dependencies: ["sasquatch"],
kind: "missing-extractor-dependency",
},
{
input_path: "$input",
provider_id: "stage-id",
handler: "gzip",
dependencies: [],
kind: "extractor-failure",
},
]);
}
},
);
const outcomes = [
{
name: "task exception",
@@ -470,7 +553,9 @@ describe("firmware producer error severity", () => {
report: owner === "multi-file" ? observation : report,
});
if (owner === "chunk") {
expect(normalized.chunks[0]?.extraction_failed).toBe(failed);
expect(normalized.chunks[0]?.extraction_failure_kinds).toEqual(
failed ? ["extractor-failure"] : [],
);
expect(normalized.failed_extraction_stages).toEqual([]);
} else {
expect(normalized.failed_extraction_stages).toEqual(
+36 -25
View File
@@ -65,8 +65,21 @@ const missingDependency = z.object({
// 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);
type ExtractionFailureKind =
| "missing-extractor-dependency"
| "extractor-failure";
const extractionFailureKinds = (
reports: readonly unknown[],
): ExtractionFailureKind[] => {
const kinds = new Set<ExtractionFailureKind>();
for (const item of reports) {
if (missingDependency.safeParse(item).success)
kinds.add("missing-extractor-dependency");
else if (extractionError.safeParse(item).success)
kinds.add("extractor-failure");
}
return [...kinds];
};
const region = z.object({
id: z.string(),
start_offset: integer,
@@ -240,7 +253,7 @@ export const normalizeUnblobReport = (
handler: string | null;
encrypted: boolean | null;
root_file_range: { offset: number; length: number } | null;
extraction_failed: boolean;
extraction_failure_kinds: ExtractionFailureKind[];
}[] = [];
const seen = new Set<string>();
const taskFailures = new Set<string>();
@@ -365,8 +378,7 @@ export const normalizeUnblobReport = (
);
noteMissingDependencies(path, [item]);
if (
hasExtractionError([item]) &&
!missingDependency.safeParse(item).success &&
extractionFailureKinds([item]).includes("extractor-failure") &&
!taskFailures.has(path)
) {
taskFailures.add(path);
@@ -402,8 +414,9 @@ export const normalizeUnblobReport = (
})
.parse(item)
: null;
const extractionFailed =
detail !== null && hasExtractionError(detail.extraction_reports);
const failures = extractionFailureKinds(
detail?.extraction_reports ?? [],
);
if (detail !== null)
for (const problem of detail.extraction_reports) {
diagnostics.push(
@@ -426,7 +439,7 @@ export const normalizeUnblobReport = (
range: { offset: parsed.start_offset, length: parsed.size },
handler: detail?.handler_name ?? null,
encrypted: detail?.is_encrypted ?? null,
extraction_failed: extractionFailed,
extraction_failure_kinds: failures,
root_file_range:
path === "$input"
? {
@@ -453,28 +466,26 @@ 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[] }]
: dependencyRows(dependencies);
const rows = [
...(dependencies === null
? []
: dependencyRows(dependencies).map((row) => ({
...row,
kind: "missing-extractor-dependency" as const,
}))),
...(extractionFailureKinds(parsed.extraction_reports).includes(
"extractor-failure",
)
? [{ dependencies: [], kind: "extractor-failure" as const }]
: []),
];
for (const row of rows)
failedExtractionStages.push({
input_path: path,
provider_id: parsed.id,
handler: parsed.handler_name,
dependencies:
dependencies === null
? []
: uniqueSorted([...row.dependencies]),
kind:
dependencies === null
? "extractor-failure"
: "missing-extractor-dependency",
dependencies: row.dependencies,
kind: row.kind,
});
}
}
@@ -567,6 +567,35 @@ it.each(["task-error", "task-warning"])(
},
);
it("retains independent failures beside dependencies at each extraction owner", async () => {
const fixture = await firmwareFixture("mixed-failures");
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 gaps = gapsWithContinuations(normalized);
expect(gaps).toHaveLength(4);
for (const provider_id of ["gzip-id", "mixed-id"]) {
const owned = gaps.filter((gap) => gap.provider_id === provider_id);
expect(owned.map((gap) => gap.kind).sort()).toEqual([
"extractor-failure",
"missing-extractor-dependency",
]);
for (const gap of owned) {
expect(gap.input_path).toBe("$input");
expect(gap.range).toEqual(
provider_id === "mixed-id" ? null : normalized.chunks[0]?.range,
);
}
}
expect(normalized.chunks[0]?.extraction_failed).toBe(true);
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", {
+25
View File
@@ -359,6 +359,31 @@ if (args.includes("--version")) {
subtasks: [childTask],
},
];
if (mode === "mixed-failures") {
const reports = [
{
__typename__: "ExtractorDependencyNotFoundReport",
severity: "ERROR",
dependencies: ["sasquatch"],
},
{
__typename__: "UnknownError",
severity: "ERROR",
exception: "independent extraction failure",
},
];
records[1].reports.find(
(report) => report.__typename__ === "ChunkReport",
).extraction_reports = reports;
records[1].reports.push({
__typename__: "MultiFileReport",
id: "mixed-id",
handler_name: "tar",
name: "rootfs.tar",
paths: [],
extraction_reports: reports,
});
}
if (mode === "many-unknown") {
records[1].reports = [
...records[1].reports,