mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(cli): preserve selected JSON members and Markdown table text
Preserve boundary facts and uncertainty through their consumers. Add focused production-boundary regressions and update affected documentation. Fixes #6756 Fixes #6752 Fixes #6755
This commit is contained in:
1 parent
52b0a54afd
commit
17cd07342d
7 files changed
+411
-18
No files matched your search
@@ -592,10 +592,17 @@ redirected, which keeps large results smaller for agents and scripts; pipe
|
||||
through `jq .` for indented files. Output selection and formatting do not change
|
||||
operation status.
|
||||
|
||||
`--format md` escapes backslashes and pipes in table headers and cells, and
|
||||
renders cell line breaks as `<br>`. These are presentation changes; retained
|
||||
Evidence and JSON output keep the original values. Table widths are measured
|
||||
iteratively, so an admitted row page is not limited by the JavaScript function
|
||||
argument count.
|
||||
|
||||
`--filter-output` selects fields from successful command data. Classified failures
|
||||
retain their complete typed diagnostic projection even when the selector matches
|
||||
a field such as `code` or `message`, or names a missing field. `--full-output`
|
||||
controls whether the outer `{ok, error, meta}` envelope is included.
|
||||
Selected own JSON members, including `__proto__`, remain ordinary output data.
|
||||
|
||||
Sibling array slices that select different fields are combined by original row
|
||||
index and emitted in source order, regardless of selector order. Selecting a
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Markdown output layout is adapted from Incur's Formatter.
|
||||
* Copyright (c) 2025-present weth, LLC
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
import { Formatter } from "incur";
|
||||
|
||||
/** Keep REA's Markdown layout compatible with Incur while owning table safety. */
|
||||
export const formatCliOutput = (
|
||||
value: unknown,
|
||||
format: Formatter.Format,
|
||||
): string =>
|
||||
format === "md" ? formatMarkdown(value) : Formatter.format(value, format);
|
||||
|
||||
const isScalar = (value: unknown): boolean =>
|
||||
value === null || value === undefined || typeof value !== "object";
|
||||
|
||||
const isFlatObject = (value: Record<string, unknown>): boolean =>
|
||||
Object.values(value).every(isScalar);
|
||||
|
||||
const isObjectArray = (value: unknown): value is Record<string, unknown>[] =>
|
||||
Array.isArray(value) &&
|
||||
value.length > 0 &&
|
||||
value.every(
|
||||
(item) => typeof item === "object" && item !== null && !Array.isArray(item),
|
||||
);
|
||||
|
||||
const escapeTableCell = (value: string): string =>
|
||||
value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\|/g, "\\|")
|
||||
.replace(/\r\n|\r|\n/g, "<br>");
|
||||
|
||||
const renderTable = (
|
||||
headers: readonly string[],
|
||||
rows: readonly string[][],
|
||||
): string => {
|
||||
const escapedHeaders = headers.map(escapeTableCell);
|
||||
const widths = escapedHeaders.map((header) => header.length);
|
||||
for (const row of rows) {
|
||||
for (let index = 0; index < escapedHeaders.length; index += 1) {
|
||||
const width = escapeTableCell(row[index] ?? "").length;
|
||||
if (width > widths[index]!) widths[index] = width;
|
||||
}
|
||||
}
|
||||
|
||||
const pad = (value: string, index: number): string =>
|
||||
value.padEnd(widths[index]!);
|
||||
const lines = [
|
||||
`| ${escapedHeaders.map(pad).join(" | ")} |`,
|
||||
`|${widths.map((width) => "-".repeat(width + 2)).join("|")}|`,
|
||||
];
|
||||
for (const row of rows) {
|
||||
lines.push(
|
||||
`| ${escapedHeaders
|
||||
.map((_, index) => pad(escapeTableCell(row[index] ?? ""), index))
|
||||
.join(" | ")} |`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
const formatKeyValueTable = (value: Record<string, unknown>): string =>
|
||||
renderTable(
|
||||
["Key", "Value"],
|
||||
Object.entries(value).map(([key, child]) => [key, String(child)]),
|
||||
);
|
||||
|
||||
const formatObjectTable = (
|
||||
items: readonly Record<string, unknown>[],
|
||||
): string => {
|
||||
const headers: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of items)
|
||||
for (const key of Object.keys(item))
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
headers.push(key);
|
||||
}
|
||||
return renderTable(
|
||||
headers,
|
||||
items.map((item) => headers.map((key) => String(item[key] ?? ""))),
|
||||
);
|
||||
};
|
||||
|
||||
const formatMarkdown = (
|
||||
value: unknown,
|
||||
path: readonly string[] = [],
|
||||
): string => {
|
||||
if (value == null && path.length === 0) return "";
|
||||
if (isScalar(value)) {
|
||||
if (path.length === 0) return String(value);
|
||||
return `## ${path.join(".")}\n\n${String(value)}`;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
if (isObjectArray(value)) {
|
||||
const table = formatObjectTable(value);
|
||||
return path.length === 0 ? table : `## ${path.join(".")}\n\n${table}`;
|
||||
}
|
||||
return formatMarkdown(String(value), path);
|
||||
}
|
||||
|
||||
const object = value as Record<string, unknown>;
|
||||
const entries = Object.entries(object);
|
||||
if (path.length === 0 && isFlatObject(object))
|
||||
return formatKeyValueTable(object);
|
||||
|
||||
const needsHeadings =
|
||||
path.length > 0 ||
|
||||
entries.length > 1 ||
|
||||
entries.some(([, child]) => !isScalar(child));
|
||||
if (!needsHeadings) return formatKeyValueTable(object);
|
||||
|
||||
return entries
|
||||
.map(([key, child]) => {
|
||||
const childPath = [...path, key];
|
||||
const heading = `## ${childPath.join(".")}\n\n`;
|
||||
if (isScalar(child)) return `${heading}${String(child)}`;
|
||||
if (isObjectArray(child)) return `${heading}${formatObjectTable(child)}`;
|
||||
if (
|
||||
typeof child === "object" &&
|
||||
child !== null &&
|
||||
!Array.isArray(child)
|
||||
) {
|
||||
const nested = child as Record<string, unknown>;
|
||||
return isFlatObject(nested)
|
||||
? `${heading}${formatKeyValueTable(nested)}`
|
||||
: formatMarkdown(nested, childPath);
|
||||
}
|
||||
return `${heading}${String(child)}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
};
|
||||
+35
-18
@@ -1,7 +1,7 @@
|
||||
import { constants } from "node:buffer";
|
||||
import type { Writable } from "node:stream";
|
||||
|
||||
import { Filter, Formatter } from "incur";
|
||||
import { Filter } from "incur";
|
||||
import { estimateTokenCount } from "tokenx";
|
||||
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
projectAnalysisError,
|
||||
type AnalysisErrorProjection,
|
||||
} from "../domain/analysisErrorProjection.js";
|
||||
import { formatCliOutput } from "./cliOutputFormatter.js";
|
||||
import {
|
||||
jsonOutputParts,
|
||||
writeJsonOutput,
|
||||
@@ -164,7 +165,7 @@ export const createCliResultOutput = (
|
||||
: error;
|
||||
// Recovery must remain actionable even when a token window/count was
|
||||
// requested. Do not apply those controls to this small typed failure.
|
||||
const text = Formatter.format(document, format);
|
||||
const text = formatCliOutput(document, format);
|
||||
handled = true;
|
||||
try {
|
||||
await writeOutputText(destination, `${text}\n`);
|
||||
@@ -247,9 +248,10 @@ export const createCliResultOutput = (
|
||||
!options.tokenCount &&
|
||||
format !== "json" &&
|
||||
format !== "jsonl";
|
||||
// Delegate unfiltered successful non-JSON rendering to Incur. This boundary
|
||||
// owns filtered successes, requested failure projections, and token windows.
|
||||
// Delegate other human formats to Incur. Markdown stays here so admission
|
||||
// and emitted table text use the same safe formatter.
|
||||
if (
|
||||
format !== "md" &&
|
||||
format !== "json" &&
|
||||
format !== "jsonl" &&
|
||||
operationFailure === undefined &&
|
||||
@@ -280,7 +282,7 @@ export const createCliResultOutput = (
|
||||
}
|
||||
}
|
||||
if (characters <= constants.MAX_STRING_LENGTH) {
|
||||
const count = estimateTokenCount(Formatter.format(tokenValue, format));
|
||||
const count = estimateTokenCount(formatCliOutput(tokenValue, format));
|
||||
handled = true;
|
||||
try {
|
||||
await writeOutputText(destination, `${count}\n`);
|
||||
@@ -334,7 +336,7 @@ export const createCliResultOutput = (
|
||||
? {}
|
||||
: filtered;
|
||||
if (ownsSuccessfulTokenWindow) {
|
||||
const formatted = Formatter.format(filtered, format);
|
||||
const formatted = formatCliOutput(filtered, format);
|
||||
const window = sliceTextByTokenWindow(
|
||||
formatted,
|
||||
options.tokenOffset ?? 0,
|
||||
@@ -349,7 +351,7 @@ export const createCliResultOutput = (
|
||||
? `${window.text}\n[truncated: showing tokens ${options.tokenOffset ?? 0}–${window.end} of ${window.total}]`
|
||||
: formatted;
|
||||
const outputDocument = options.fullOutput
|
||||
? Formatter.format(
|
||||
? formatCliOutput(
|
||||
window.truncated
|
||||
? {
|
||||
ok: true,
|
||||
@@ -383,11 +385,20 @@ export const createCliResultOutput = (
|
||||
// Once writing starts, a failed destination must not receive a second
|
||||
// document from Incur's error formatter.
|
||||
const formattedText =
|
||||
(operationFailure !== undefined || ownsFilteredOutput) &&
|
||||
format !== "json" &&
|
||||
format !== "jsonl"
|
||||
? Formatter.format(document, format)
|
||||
format === "md" ||
|
||||
((operationFailure !== undefined || ownsFilteredOutput) &&
|
||||
format !== "json" &&
|
||||
format !== "jsonl")
|
||||
? formatCliOutput(document, format)
|
||||
: undefined;
|
||||
if (
|
||||
format === "md" &&
|
||||
formattedText === "" &&
|
||||
!options.fullOutput &&
|
||||
!ownsFilteredOutput &&
|
||||
operationFailure === undefined
|
||||
)
|
||||
return false;
|
||||
handled = true;
|
||||
try {
|
||||
if (formattedText !== undefined) {
|
||||
@@ -422,7 +433,7 @@ const fullFailureEnvelope = (
|
||||
): Readonly<Record<string, unknown>> => {
|
||||
if (tokenLimit === undefined && tokenOffset === undefined)
|
||||
return { ok: false, error: failure, meta };
|
||||
const formatted = Formatter.format(failure, format);
|
||||
const formatted = formatCliOutput(failure, format);
|
||||
const offset = tokenOffset ?? 0;
|
||||
const window = sliceTextByTokenWindow(formatted, offset, tokenLimit);
|
||||
if (!window.truncated) return { ok: false, error: failure, meta };
|
||||
@@ -449,7 +460,12 @@ const applyCliOutputFilter = (value: unknown, paths: FilterPaths): unknown => {
|
||||
"key" in paths[0][0]
|
||||
) {
|
||||
const key = paths[0][0].key;
|
||||
if (typeof value !== "object" || value === null) return undefined;
|
||||
if (
|
||||
typeof value !== "object" ||
|
||||
value === null ||
|
||||
!Object.hasOwn(value, key)
|
||||
)
|
||||
return undefined;
|
||||
const selected = (value as Record<string, unknown>)[key];
|
||||
return typeof selected !== "object" || selected === null
|
||||
? selected
|
||||
@@ -470,31 +486,32 @@ const projectObjectPaths = (value: unknown, paths: FilterPaths): unknown => {
|
||||
byKey.set(segment.key, siblings);
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
const result = new Map<string, unknown>();
|
||||
for (const key of [...byKey.keys()].sort()) {
|
||||
const siblings = byKey.get(key)!;
|
||||
if (!Object.hasOwn(value, key)) continue;
|
||||
const child = (value as Record<string, unknown>)[key];
|
||||
if (child === undefined) continue;
|
||||
// Selecting a parent retains its complete original subtree and dominates
|
||||
// any narrower selections below it, independent of path order.
|
||||
if (siblings.some((path) => path.length === 0)) {
|
||||
result[key] = child;
|
||||
result.set(key, child);
|
||||
continue;
|
||||
}
|
||||
const slicePaths = siblings.filter(
|
||||
(path) => path[0] !== undefined && "start" in path[0],
|
||||
);
|
||||
if (slicePaths.length > 0 && Array.isArray(child)) {
|
||||
result[key] = projectArraySlices(child, slicePaths);
|
||||
result.set(key, projectArraySlices(child, slicePaths));
|
||||
continue;
|
||||
}
|
||||
const objectPaths = siblings.filter(
|
||||
(path) => path[0] !== undefined && "key" in path[0],
|
||||
);
|
||||
if (objectPaths.length > 0 && typeof child === "object" && child !== null)
|
||||
result[key] = projectObjectPaths(child, objectPaths);
|
||||
result.set(key, projectObjectPaths(child, objectPaths));
|
||||
}
|
||||
return result;
|
||||
return Object.fromEntries(result);
|
||||
};
|
||||
|
||||
/** Union sliced rows by their original index, then emit them in source order. */
|
||||
|
||||
@@ -552,6 +552,92 @@ cliTest(
|
||||
expect(complete.json).toEqual(bundle);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"formats the full caller-admitted native inventory page as Markdown and JSON",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-native-inventory-markdown-",
|
||||
);
|
||||
const input = join(root, "view.json");
|
||||
const rowCount = 150_000;
|
||||
{
|
||||
const parent = createEvidence(
|
||||
{
|
||||
path: "/fixtures/native.exe",
|
||||
format: "pe",
|
||||
sha256: "a".repeat(64),
|
||||
},
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "list_names",
|
||||
parameters: {},
|
||||
result: Array.from({ length: rowCount }, (_, index) => ({
|
||||
address: `0x${index.toString(16)}`,
|
||||
value: `item${index}`,
|
||||
})),
|
||||
},
|
||||
);
|
||||
const request = {
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: {
|
||||
kind: "native-inventory",
|
||||
limit: rowCount,
|
||||
fields: ["value"],
|
||||
},
|
||||
};
|
||||
await writeFile(input, JSON.stringify(request));
|
||||
}
|
||||
const environment = {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
};
|
||||
|
||||
{
|
||||
const json = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
input,
|
||||
"--format",
|
||||
"json",
|
||||
"--filter-output",
|
||||
"normalized_result.rows",
|
||||
],
|
||||
environment,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(json.exitCode).toBe(0);
|
||||
expect(
|
||||
(json.json as { normalized_result: { rows: unknown[] } })
|
||||
.normalized_result.rows,
|
||||
).toHaveLength(rowCount);
|
||||
}
|
||||
|
||||
const markdown = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
input,
|
||||
"--format",
|
||||
"md",
|
||||
"--filter-output",
|
||||
"normalized_result.rows",
|
||||
],
|
||||
environment,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(markdown.exitCode).toBe(0);
|
||||
expect(markdown.stdout).toContain("## normalized_result.rows");
|
||||
expect(markdown.stdout).toContain("| source_index");
|
||||
let lineBreaks = 0;
|
||||
for (const character of markdown.stdout)
|
||||
if (character === "\n") lineBreaks += 1;
|
||||
expect(lineBreaks).toBeGreaterThan(rowCount);
|
||||
expect(markdown.stdout).toContain("149999");
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"projects one section from a JSON file of inline layout Evidence",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import {
|
||||
@@ -147,3 +148,75 @@ cliTest(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves a selected own __proto__ member from complete bundle data",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-evidence-filter-proto-");
|
||||
const path = join(root, "bundle.json");
|
||||
const result = jsonValueSchema.parse(
|
||||
JSON.parse('{"__proto__":{"answer":7},"normal":{"answer":7}}'),
|
||||
);
|
||||
const record = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{ operation: "ordinary-json", parameters: {}, result },
|
||||
);
|
||||
await writeFile(path, JSON.stringify(createEvidenceBundle([record])));
|
||||
|
||||
const output = await cli.run({
|
||||
arguments: [
|
||||
"inspect-evidence-bundle",
|
||||
JSON.stringify({ path, detail: "complete" }),
|
||||
"--json",
|
||||
"--filter-output",
|
||||
"records[0,1].normalized_result.__proto__.answer",
|
||||
],
|
||||
});
|
||||
|
||||
expect(output.exitCode).toBe(0);
|
||||
const records = (
|
||||
output.json as {
|
||||
records: Array<{ normalized_result: unknown }>;
|
||||
}
|
||||
).records;
|
||||
const normalizedResult = records[0]?.normalized_result;
|
||||
expect(typeof normalizedResult).toBe("object");
|
||||
expect(normalizedResult).not.toBeNull();
|
||||
expect(Object.hasOwn(normalizedResult as object, "__proto__")).toBe(true);
|
||||
expect((normalizedResult as Record<string, unknown>)["__proto__"]).toEqual({
|
||||
answer: 7,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves cell text when formatting retained Evidence as Markdown",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-evidence-markdown-cells-");
|
||||
const path = join(root, "bundle.json");
|
||||
const result = { text: "left|right\\tail\nnext" };
|
||||
const record = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "ordinary-json",
|
||||
parameters: {},
|
||||
result,
|
||||
},
|
||||
);
|
||||
await writeFile(path, JSON.stringify(createEvidenceBundle([record])));
|
||||
const base = [
|
||||
"inspect-evidence-bundle",
|
||||
JSON.stringify({ path, detail: "complete" }),
|
||||
"--filter-output",
|
||||
"records.0.normalized_result",
|
||||
];
|
||||
const json = await cli.run({ arguments: [...base, "--json"] });
|
||||
expect(json.exitCode, json.stderr).toBe(0);
|
||||
expect(json.stdout).toContain(JSON.stringify(result.text));
|
||||
const markdown = await cli.run({ arguments: [...base, "--format", "md"] });
|
||||
expect(markdown.exitCode, markdown.stderr).toBe(0);
|
||||
expect(markdown.stdout).toContain("left\\|right\\\\tail<br>next");
|
||||
},
|
||||
);
|
||||
@@ -210,6 +210,35 @@ it("finishes JSONL after a bounded large record before a missing selection", asy
|
||||
expect(output.failed).toBe(false);
|
||||
});
|
||||
|
||||
it("does not invent selected JSON members from the object prototype", async () => {
|
||||
const value = freezeRecursively({ selected: { present: 7 } });
|
||||
const output = await runFilteredCli(value, "json", "selected.__proto__");
|
||||
expect(JSON.parse(output)).toEqual({ selected: {} });
|
||||
expect(value).toEqual({ selected: { present: 7 } });
|
||||
});
|
||||
|
||||
it("escapes table delimiters and line breaks in Markdown cells", async () => {
|
||||
const value = freezeRecursively({
|
||||
selected: {
|
||||
pipe: "left|right",
|
||||
backslash: "left\\right",
|
||||
lines: "first line\nsecond line",
|
||||
},
|
||||
});
|
||||
|
||||
const output = await runFilteredCli(value, "md", "selected");
|
||||
|
||||
expect(output).toContain("left\\|right");
|
||||
expect(output).toContain("left\\\\right");
|
||||
expect(output).toContain("first line<br>second line");
|
||||
expect(output).not.toContain("left|right");
|
||||
expect(value.selected).toEqual({
|
||||
pipe: "left|right",
|
||||
backslash: "left\\right",
|
||||
lines: "first line\nsecond line",
|
||||
});
|
||||
});
|
||||
|
||||
const projectionSource = freezeRecursively([
|
||||
{
|
||||
rows: [
|
||||
|
||||
@@ -120,6 +120,37 @@ describe("CLI formatting stage admission", () => {
|
||||
});
|
||||
|
||||
describe("CLI formatting recovery outcomes", () => {
|
||||
it("formats a recovered Markdown result through the CLI Markdown owner", async () => {
|
||||
const cause = new RangeError("Invalid string length");
|
||||
const chunks: Buffer[] = [];
|
||||
const output = createCliResultOutput(
|
||||
["--format", "md"],
|
||||
new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
);
|
||||
const value = new Proxy(
|
||||
{},
|
||||
{
|
||||
ownKeys: () => {
|
||||
throw cause;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await output?.write(value, { ...metadata, format: "md" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(output?.error).toMatchObject({ code: "resource_constraint" });
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
expect(text).toContain("resource_constraint");
|
||||
expect(text).toContain("original result was not written");
|
||||
expect(output?.failed).toBe(false);
|
||||
});
|
||||
|
||||
it("retains ownership if the recovery document cannot be written", async () => {
|
||||
const cause = new RangeError("Invalid string length");
|
||||
const output = createCliResultOutput(
|
||||
|
||||
Reference in new issue
Block a user