fix: attribute rendered frame omissions to their producing stage
This commit is contained in:
1 parent
a0fcc60d89
commit
5934387f41
7 files changed
+70
-13
No files matched your search
@@ -433,10 +433,11 @@ Every capture requires `truncation_details`, with separate accounting for:
|
||||
bytes, plus observed/retained frame counts. This is independent of the raw
|
||||
chunk budget and does not count JSON encoding. Repeated screen snapshots can
|
||||
exhaust it even when all raw output fits. Rendered states use retained PTY
|
||||
input, so raw omissions also limit rendered coverage. When bounded
|
||||
normalization omits a rendered frame before its final size is known,
|
||||
`unknown_size_frames` reports that count; `observed_bytes` includes only
|
||||
rendered frame sizes that were determined exactly.
|
||||
input, so raw omissions also limit rendered coverage. `unknown_size_frames`
|
||||
counts frames whose complete size was not calculated; it does not by itself
|
||||
identify normalization as the cause. `normalization_over_budget_frames`
|
||||
identifies omissions caused by an ordered normalization stage.
|
||||
`observed_bytes` includes only rendered frame sizes determined exactly.
|
||||
- `filesystem_before` and `filesystem_after`: file-count/depth limits,
|
||||
enumeration failures, whole-file hash budget and bytes successfully hashed.
|
||||
Directory names are read incrementally within the remaining entry capacity.
|
||||
|
||||
@@ -9,6 +9,7 @@ const byteRetentionSchema = z.strictObject({
|
||||
observed_frames: z.int().nonnegative(),
|
||||
retained_frames: z.int().nonnegative(),
|
||||
unknown_size_frames: z.int().nonnegative().optional(),
|
||||
normalization_over_budget_frames: z.int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
const enumerationOmissionReasonSchema = z.enum([
|
||||
@@ -60,7 +61,7 @@ export const processCaptureTruncationDetailsSchema = z.strictObject({
|
||||
"UTF-8 bytes of original PTY chunks; whole chunks are retained or omitted.",
|
||||
),
|
||||
rendered_terminal: byteRetentionSchema.describe(
|
||||
"Cumulative UTF-8 bytes of serialized state plus visible lines, evaluated from retained PTY input; whole frames are retained or omitted. When normalization exceeds a bound before the frame size is known, observed_bytes contains the exact total for size-known frames and unknown_size_frames counts the other omissions.",
|
||||
"Cumulative UTF-8 bytes of serialized state plus visible lines, evaluated from retained PTY input; whole frames are retained or omitted. observed_bytes contains the exact total for size-known frames; unknown_size_frames counts frames whose complete size was not calculated. normalization_over_budget_frames identifies the subset stopped by an ordered normalization stage rather than final representation retention.",
|
||||
),
|
||||
normalized_terminal: z
|
||||
.strictObject({
|
||||
|
||||
@@ -455,7 +455,7 @@ const captureCoverageUnknowns = (
|
||||
unknowns.push({
|
||||
scope: "terminal",
|
||||
reason:
|
||||
(details.rendered_terminal.unknown_size_frames ?? 0) > 0
|
||||
(details.rendered_terminal.normalization_over_budget_frames ?? 0) > 0
|
||||
? "Rendered frames were omitted because a normalization stage exceeded the remaining output_bytes budget; final rendered byte totals are unknown for those frames. See truncation_details.rendered_terminal."
|
||||
: "Cumulative rendered state and visible-line bytes exceeded output_bytes; whole rendered frames were omitted. Raw PTY coverage is reported separately. See truncation_details.rendered_terminal.",
|
||||
});
|
||||
|
||||
@@ -108,7 +108,10 @@ it("bounds ordered normalization before replacement expansion and preserves late
|
||||
rootPid,
|
||||
);
|
||||
|
||||
expect(normalize("a".repeat(100), 1_000)).toEqual({ state: "over_budget" });
|
||||
expect(normalize("a".repeat(100), 1_000)).toEqual({
|
||||
state: "over_budget",
|
||||
reason: "normalization",
|
||||
});
|
||||
expect(normalize("z", 1_000)).toEqual({
|
||||
state: "normalized",
|
||||
value: "z",
|
||||
@@ -131,7 +134,10 @@ it("keeps ordered intermediate expansion explicit even when a later rule could s
|
||||
rootPid,
|
||||
);
|
||||
|
||||
expect(normalize("a", 10)).toEqual({ state: "over_budget" });
|
||||
expect(normalize("a", 10)).toEqual({
|
||||
state: "over_budget",
|
||||
reason: "normalization",
|
||||
});
|
||||
});
|
||||
|
||||
it("counts UTF-8 bytes across replacement boundaries like the final JavaScript string", () => {
|
||||
@@ -215,7 +221,7 @@ it("counts UTF-8 bytes across replacement boundaries like the final JavaScript s
|
||||
"/temporary",
|
||||
rootPid,
|
||||
)(input, Buffer.byteLength(expected) - 1),
|
||||
).toEqual({ state: "over_budget" });
|
||||
).toEqual({ state: "over_budget", reason: "normalization" });
|
||||
|
||||
const loneSurrogate = scenario({
|
||||
paths: false,
|
||||
|
||||
@@ -10,7 +10,10 @@ export type BoundedProcessTextNormalization =
|
||||
readonly value: string;
|
||||
readonly utf8Bytes: number;
|
||||
}
|
||||
| { readonly state: "over_budget" };
|
||||
| {
|
||||
readonly state: "over_budget";
|
||||
readonly reason: "normalization" | "retention";
|
||||
};
|
||||
|
||||
type RegexReplacement = (match: string, ...groups: string[]) => string;
|
||||
|
||||
@@ -270,7 +273,8 @@ export const createBoundedProcessTextNormalizer = (
|
||||
rule.replacement,
|
||||
maxBytes,
|
||||
);
|
||||
if (result.state === "over_budget") return result;
|
||||
if (result.state === "over_budget")
|
||||
return { state: "over_budget", reason: "normalization" };
|
||||
normalized = result.value;
|
||||
}
|
||||
const identity = boundedIdentity(normalized, maxBytes);
|
||||
@@ -280,7 +284,7 @@ export const createBoundedProcessTextNormalizer = (
|
||||
value: identity.value,
|
||||
utf8Bytes: identity.utf8Bytes,
|
||||
}
|
||||
: { state: "over_budget" };
|
||||
: { state: "over_budget", reason: "retention" };
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const boundedNormalize =
|
||||
const utf8Bytes = Buffer.byteLength(normalized);
|
||||
return utf8Bytes <= maxBytes
|
||||
? { state: "normalized", value: normalized, utf8Bytes }
|
||||
: { state: "over_budget" };
|
||||
: { state: "over_budget", reason: "retention" };
|
||||
};
|
||||
|
||||
const renderFrames = async (
|
||||
@@ -152,6 +152,7 @@ it("omits rendered frames with unknown expanded size and keeps later bounded sta
|
||||
observed_frames: 2,
|
||||
retained_frames: 1,
|
||||
unknown_size_frames: 1,
|
||||
normalization_over_budget_frames: 1,
|
||||
});
|
||||
expect(renderer.truncated()).toBe(true);
|
||||
} finally {
|
||||
@@ -248,3 +249,36 @@ it("retains disposal failures for a later cleanup retry", async () => {
|
||||
await rm(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([1024, 2048])(
|
||||
"keeps unchanged representation retention distinct from normalization at budget %s",
|
||||
async (maxBytes) => {
|
||||
const renderer = new TerminalRenderer({
|
||||
columns: 600,
|
||||
rows: 1,
|
||||
scrollback: 0,
|
||||
maxBytes,
|
||||
normalize: createBoundedProcessTextNormalizer(
|
||||
parseProcessScenario({
|
||||
executable: "unused",
|
||||
normalization: { paths: false, pids: false, ports: false },
|
||||
}),
|
||||
"/tmp",
|
||||
-1,
|
||||
),
|
||||
});
|
||||
try {
|
||||
renderer.write("a".repeat(600), 0);
|
||||
const frames = await renderer.frames();
|
||||
expect(frames).toHaveLength(maxBytes === 1024 ? 0 : 1);
|
||||
expect(renderer.retention().normalization_over_budget_frames ?? 0).toBe(
|
||||
0,
|
||||
);
|
||||
expect(renderer.retention().unknown_size_frames ?? 0).toBe(
|
||||
maxBytes === 1024 ? 1 : 0,
|
||||
);
|
||||
} finally {
|
||||
await renderer.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -47,6 +47,7 @@ export class TerminalRenderer {
|
||||
#observedFrames = 0;
|
||||
#truncated = false;
|
||||
#unknownSizeFrames = 0;
|
||||
#normalizationOverBudgetFrames = 0;
|
||||
|
||||
constructor(private readonly options: TerminalRendererOptions) {
|
||||
this.#terminal = new HeadlessPackage.Terminal({
|
||||
@@ -105,6 +106,12 @@ export class TerminalRenderer {
|
||||
...(this.#unknownSizeFrames === 0
|
||||
? {}
|
||||
: { unknown_size_frames: this.#unknownSizeFrames }),
|
||||
...(this.#normalizationOverBudgetFrames === 0
|
||||
? {}
|
||||
: {
|
||||
normalization_over_budget_frames:
|
||||
this.#normalizationOverBudgetFrames,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,6 +149,8 @@ export class TerminalRenderer {
|
||||
Math.min(Number.MAX_SAFE_INTEGER, remainingBytes + trailingSpaceBytes),
|
||||
);
|
||||
if (normalized.state === "over_budget") {
|
||||
if (normalized.reason === "normalization")
|
||||
this.#normalizationOverBudgetFrames += 1;
|
||||
this.#unknownSizeFrames += 1;
|
||||
this.#truncated = true;
|
||||
return;
|
||||
@@ -164,6 +173,8 @@ export class TerminalRenderer {
|
||||
remainingBytes,
|
||||
);
|
||||
if (normalizedState.state === "over_budget") {
|
||||
if (normalizedState.reason === "normalization")
|
||||
this.#normalizationOverBudgetFrames += 1;
|
||||
this.#unknownSizeFrames += 1;
|
||||
this.#truncated = true;
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user