mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(json): consolidate dictionary admission across analysis and verification
This commit is contained in:
@@ -1,28 +0,0 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
export interface StructuredScopeExpectation {
|
||||
readonly operation:
|
||||
| "parse-package-json"
|
||||
| "parse-json-module"
|
||||
| "parse-html-script-references"
|
||||
| "parse-local-source-map";
|
||||
readonly analysis_status: "interrupted" | "not-started";
|
||||
}
|
||||
|
||||
export interface StructuredProcessingScope {
|
||||
readonly properties: Readonly<Record<string, unknown>>;
|
||||
readonly evidence: {
|
||||
readonly authority: string;
|
||||
readonly state: string;
|
||||
readonly confidence: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const structuredScopeExpectationsSchema: z.ZodType<
|
||||
Record<string, StructuredScopeExpectation>
|
||||
>;
|
||||
|
||||
export function matchesStructuredScopeExpectation(
|
||||
scopes: readonly StructuredProcessingScope[],
|
||||
expected: StructuredScopeExpectation | undefined,
|
||||
): boolean;
|
||||
+20
-3
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { jsonRecordSchema } from "../../src/domain/jsonRecord.js";
|
||||
|
||||
/** Independent expected outcomes supplied by the verification scenario. */
|
||||
export const structuredScopeExpectationsSchema = z.record(
|
||||
z.string().min(1),
|
||||
export const structuredScopeExpectationsSchema = jsonRecordSchema(
|
||||
z.strictObject({
|
||||
operation: z.enum([
|
||||
"parse-package-json",
|
||||
@@ -12,10 +12,27 @@ export const structuredScopeExpectationsSchema = z.record(
|
||||
]),
|
||||
analysis_status: z.enum(["interrupted", "not-started"]),
|
||||
}),
|
||||
z.string().min(1),
|
||||
);
|
||||
|
||||
export type StructuredScopeExpectation = z.output<
|
||||
typeof structuredScopeExpectationsSchema
|
||||
>[string];
|
||||
|
||||
export interface StructuredProcessingScope {
|
||||
readonly properties: Readonly<Record<string, unknown>>;
|
||||
readonly evidence: {
|
||||
readonly authority: string;
|
||||
readonly state: string;
|
||||
readonly confidence: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** Every retained gap must match the named phase and its unavailable status. */
|
||||
export const matchesStructuredScopeExpectation = (scopes, expected) =>
|
||||
export const matchesStructuredScopeExpectation = (
|
||||
scopes: readonly StructuredProcessingScope[],
|
||||
expected: StructuredScopeExpectation | undefined,
|
||||
): boolean =>
|
||||
expected !== undefined &&
|
||||
scopes.length > 0 &&
|
||||
scopes.every(
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
runOwnedCommand,
|
||||
} from "../../../dist/process/OwnedCommand.js";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
import {
|
||||
matchesStructuredScopeExpectation,
|
||||
structuredScopeExpectationsSchema,
|
||||
} from "../../lib/javascript-file-scope-oracle.mjs";
|
||||
import { importVerifierSource } from "../../lib/verifier-source-loader.mjs";
|
||||
|
||||
const { matchesStructuredScopeExpectation, structuredScopeExpectationsSchema } =
|
||||
await importVerifierSource(
|
||||
new URL("../../lib/javascript-file-scope-oracle.mts", import.meta.url).href,
|
||||
);
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { analysisDetailSchema } from "./analysisDetail.js";
|
||||
import { z } from "zod";
|
||||
import { jsonRecordSchema } from "../domain/jsonRecord.js";
|
||||
import { nativeFunctionAnnotationsInputSchema } from "../domain/native/nativeFunctionAnnotations.js";
|
||||
|
||||
import { officialOutputSchemas } from "./toolOutputSchemaGroups.js";
|
||||
@@ -220,7 +221,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
official(
|
||||
"set_addresses_names",
|
||||
"Assign analyst names to multiple mapped addresses in one call and return per-address edit/readback success booleans. Hopper validates mapped destinations, native string representations, its 1024 UTF-16 code-unit symbol-name limit, and unique destinations/names before applying edits. Existing labels may move or swap only among explicitly selected addresses; unselected label owners remain unchanged. This mutates analysis metadata; verify failures individually.",
|
||||
z.object({ names: z.record(z.string(), z.string()), document }),
|
||||
z.object({ names: jsonRecordSchema(z.string()), document }),
|
||||
),
|
||||
official(
|
||||
"set_bookmark",
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { Evidence } from "../evidence.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { webNetworkCaptureSchema } from "../webNetworkCapture.js";
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
projectAnalysisView,
|
||||
type AnalysisViewParent,
|
||||
type AnalysisViewResult,
|
||||
@@ -29,6 +30,8 @@ import {
|
||||
type JsonObject,
|
||||
type StateRecordFacts,
|
||||
} from "./environmentStateRules.js";
|
||||
import { evaluateQuery } from "./environmentStateListQuery.js";
|
||||
import { singleQueryValues } from "../recordedTransportUrl.js";
|
||||
import { environmentStateFields } from "./environmentStateView.js";
|
||||
import {
|
||||
lineShapes,
|
||||
@@ -1725,3 +1728,65 @@ it.each(["+00:60", "+24:00", "+99:99"])(
|
||||
expect(environmentStateFields.views.safeParse([view]).success).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps learned prototype-named query roles through public result normalization", () => {
|
||||
const books = [
|
||||
{ id: 1, shelf: "a" },
|
||||
{ id: 2, shelf: "b" },
|
||||
];
|
||||
const state = stateOf(
|
||||
captureOf([
|
||||
["GET", "/api/books", books],
|
||||
["GET", "/api/books?__proto__=x&shelf=a", [books[0]!]],
|
||||
]),
|
||||
);
|
||||
const normalized = analysisViewResultSchema.parse(
|
||||
JSON.parse(JSON.stringify(state)),
|
||||
);
|
||||
if (normalized.kind !== "environment-state")
|
||||
throw new Error("Expected state");
|
||||
const view = normalized.views.find((entry) => entry.shape === "list");
|
||||
if (view?.shape !== "list" || view.query === undefined)
|
||||
throw new Error("Expected learned query");
|
||||
expect(
|
||||
Object.getOwnPropertyDescriptor(view.query.parameters, "__proto__")?.value,
|
||||
).toEqual({ role: "inert", values: ["x"] });
|
||||
expect(
|
||||
evaluateQuery(
|
||||
view.query.parameters,
|
||||
singleQueryValues("?__proto__=x&shelf=a")!,
|
||||
normalized.entities,
|
||||
)?.page.map((entity) => entity.id),
|
||||
).toEqual(["1"]);
|
||||
});
|
||||
|
||||
it("validates every own render-metadata dictionary entry through the public schema", () => {
|
||||
const state = stateOf();
|
||||
const item = state.views.find((view) => view.shape === "item")!;
|
||||
const parse = (clock: unknown, nested: unknown) =>
|
||||
analysisViewResultSchema.safeParse({
|
||||
...state,
|
||||
views: [
|
||||
{
|
||||
...item,
|
||||
clock_formats: Object.fromEntries([["__proto__", clock]]),
|
||||
nested_fields: Object.fromEntries([["__proto__", nested]]),
|
||||
},
|
||||
],
|
||||
});
|
||||
const clock = { kind: "epoch", unit: "ms" };
|
||||
const parsed = parse(clock, ["id"]);
|
||||
expect(parsed.success).toBe(true);
|
||||
if (!parsed.success || parsed.data.kind !== "environment-state")
|
||||
throw new Error("Expected state");
|
||||
const view = parsed.data.views[0]!;
|
||||
if (view.shape !== "item") throw new Error("Expected item");
|
||||
expect(
|
||||
Object.getOwnPropertyDescriptor(view.clock_formats, "__proto__")?.value,
|
||||
).toEqual(clock);
|
||||
expect(
|
||||
Object.getOwnPropertyDescriptor(view.nested_fields, "__proto__")?.value,
|
||||
).toEqual(["id"]);
|
||||
expect(parse({ kind: "epoch", unit: "invalid" }, ["id"]).success).toBe(false);
|
||||
expect(parse(clock, [1]).success).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { jsonRecordSchema } from "../jsonRecord.js";
|
||||
import { ok, type Result } from "../result.js";
|
||||
import type {
|
||||
AnalysisViewParent,
|
||||
@@ -73,11 +74,9 @@ const queryParameterSchema = z.discriminatedUnion("role", [
|
||||
}),
|
||||
]);
|
||||
const listQuerySchema = z.strictObject({
|
||||
parameters: z
|
||||
.record(z.string(), queryParameterSchema)
|
||||
.describe(
|
||||
"What each query parameter does. A call whose every parameter has a role is answered by evaluating them over the state.",
|
||||
),
|
||||
parameters: jsonRecordSchema(queryParameterSchema).describe(
|
||||
"What each query parameter does. A call whose every parameter has a role is answered by evaluating them over the state.",
|
||||
),
|
||||
record_ordinals: z
|
||||
.array(z.number().int().nonnegative())
|
||||
.describe("Checked lines the query reproduces, answered by evaluating it."),
|
||||
@@ -122,14 +121,12 @@ const entityViewFields = {
|
||||
required_fields: z.array(z.string()),
|
||||
checked_record_ordinals: checkedSchema,
|
||||
per_response_members: perResponseSchema,
|
||||
clock_formats: z
|
||||
.record(z.string(), clockFormatSchema)
|
||||
clock_formats: jsonRecordSchema(clockFormatSchema)
|
||||
.optional()
|
||||
.describe(
|
||||
"How the view spells clock fields it always spells otherwise than the entities' stored values; a stored value the view never recorded is spelled this way.",
|
||||
),
|
||||
nested_fields: z
|
||||
.record(z.string(), z.array(z.string()))
|
||||
nested_fields: jsonRecordSchema(z.array(z.string()))
|
||||
.optional()
|
||||
.describe(
|
||||
"The keys the view shows of object fields it always shows with fewer keys than an entity stores, such as a parent listed without its own nested type; a stored object the view never recorded shows those keys.",
|
||||
|
||||
@@ -233,3 +233,32 @@ describe("provider-neutral inclusive function body evidence", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["procedures", "strings"])(
|
||||
"counts every own entry of direct and wrapped %s maps",
|
||||
(property) => {
|
||||
for (const text of [
|
||||
'{"__proto__":"observed"}',
|
||||
'{"__proto__":"observed","0x1000":"one"}',
|
||||
]) {
|
||||
const source = JSON.parse(text);
|
||||
const count = Object.keys(source).length;
|
||||
expect(parseListCount(source, property)).toEqual({
|
||||
ok: true,
|
||||
value: count,
|
||||
});
|
||||
expect(parseListCount({ [property]: source }, property)).toEqual({
|
||||
ok: true,
|
||||
value: count,
|
||||
});
|
||||
expect(JSON.stringify(source)).toBe(text);
|
||||
}
|
||||
expect(parseListCount({ [property]: [] }, property)).toEqual({
|
||||
ok: true,
|
||||
value: 0,
|
||||
});
|
||||
expect(parseListCount({}, property)).toEqual({ ok: true, value: 0 });
|
||||
for (const value of [null, "not a list", 3, false])
|
||||
expect(parseListCount(value, property).ok).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { fixedTupleSchema } from "./fixedTupleSchema.js";
|
||||
import { addressCoordinate } from "./native/addressCoordinate.js";
|
||||
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "./jsonValue.js";
|
||||
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { nativeApiBoundarySchema } from "./native/nativeApiBoundary.js";
|
||||
@@ -577,7 +577,7 @@ export const parseListCount = (
|
||||
const unwrapped = unwrapProperty(value, property);
|
||||
const list = z.array(z.unknown()).safeParse(unwrapped);
|
||||
if (list.success) return ok(list.data.length);
|
||||
const map = z.record(z.string(), z.unknown()).safeParse(unwrapped);
|
||||
const map = jsonObjectSchema.safeParse(unwrapped);
|
||||
return map.success
|
||||
? ok(Object.keys(map.data).length)
|
||||
: invalid(operation, `${property} list`, map.error);
|
||||
|
||||
@@ -35,6 +35,34 @@ it("prefixes value failures with every original key and nested value path", () =
|
||||
]);
|
||||
});
|
||||
|
||||
it("validates every original key before projecting a constrained record", () => {
|
||||
const key = z.string().regex(/^0x[0-9a-f]+$/u);
|
||||
const schema = jsonRecordSchema(z.string(), key);
|
||||
const parsed = schema.safeParse(
|
||||
JSON.parse('{"0x1000":"entry","__proto__":"other"}'),
|
||||
);
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) throw new Error("Expected an invalid address key");
|
||||
expect(parsed.error.issues).toMatchObject([
|
||||
{ code: "invalid_key", path: ["__proto__"] },
|
||||
]);
|
||||
expect(z.toJSONSchema(schema)).toEqual(
|
||||
z.toJSONSchema(z.record(key, z.string())),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains normalized keys as own properties without mutating their source", () => {
|
||||
const source = { " __proto__ ": "observed", " ordinary ": "value" };
|
||||
const schema = jsonRecordSchema(z.string(), z.string().trim().min(1));
|
||||
const parsed = schema.parse(source);
|
||||
expect(JSON.stringify(parsed)).toBe(
|
||||
'{"__proto__":"observed","ordinary":"value"}',
|
||||
);
|
||||
expect(Object.hasOwn(parsed, "__proto__")).toBe(true);
|
||||
expect(Object.getPrototypeOf(parsed)).toBe(Object.prototype);
|
||||
expect(Object.keys(source)).toEqual([" __proto__ ", " ordinary "]);
|
||||
});
|
||||
|
||||
it("retains defaults produced within JSON entries without mutating their source", () => {
|
||||
const source: unknown = JSON.parse(
|
||||
'{"__proto__":{},"ordinary":{"name":"given"}}',
|
||||
@@ -90,6 +118,29 @@ it("preserves input schema projection for a normalizing transform", () => {
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("preserves record projection and runtime ownership through schema clones", () => {
|
||||
const schema = jsonRecordSchema(z.string(), z.string().min(1));
|
||||
const ordinary = z.record(z.string().min(1), z.string());
|
||||
const variants = [
|
||||
[schema.clone(), ordinary.clone()],
|
||||
[schema.describe("caller fields"), ordinary.describe("caller fields")],
|
||||
[schema.meta({ title: "Fields" }), ordinary.meta({ title: "Fields" })],
|
||||
[
|
||||
schema.describe("caller fields").meta({ title: "Fields" }).clone(),
|
||||
ordinary.describe("caller fields").meta({ title: "Fields" }).clone(),
|
||||
],
|
||||
] as const;
|
||||
for (const [actual, expected] of variants) {
|
||||
for (const io of ["input", "output"] as const)
|
||||
expect(z.toJSONSchema(actual, { io })).toEqual(
|
||||
z.toJSONSchema(expected, { io }),
|
||||
);
|
||||
expect(
|
||||
JSON.stringify(actual.parse(JSON.parse('{"__proto__":"observed"}'))),
|
||||
).toBe('{"__proto__":"observed"}');
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["draft-04", "draft-07", "draft-2020-12", "openapi-3.0"] as const)(
|
||||
"preserves shared schema references through Standard Schema export for %s",
|
||||
(target) => {
|
||||
|
||||
@@ -4,15 +4,27 @@ import { jsonObjectSchema, type JsonValue } from "./jsonValue.js";
|
||||
/** Own every JSON map key, then validate and retain each value's normalized facts. */
|
||||
export const jsonRecordSchema = <Value extends JsonValue>(
|
||||
valueSchema: z.ZodType<Value>,
|
||||
keySchema: z.ZodString = z.string(),
|
||||
) => {
|
||||
const projection = z.record(z.string(), valueSchema);
|
||||
const projection = z.record(keySchema, valueSchema);
|
||||
const schema = jsonObjectSchema.transform(
|
||||
(value, context): Record<string, Value> => {
|
||||
const entries: [string, Value][] = [];
|
||||
let invalid = false;
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
const parsedKey = keySchema.safeParse(key);
|
||||
if (!parsedKey.success) {
|
||||
invalid = true;
|
||||
context.addIssue({
|
||||
code: "invalid_key",
|
||||
origin: "record",
|
||||
issues: parsedKey.error.issues,
|
||||
path: [key],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const parsed = valueSchema.safeParse(entry);
|
||||
if (parsed.success) entries.push([key, parsed.data]);
|
||||
if (parsed.success) entries.push([parsedKey.data, parsed.data]);
|
||||
else {
|
||||
invalid = true;
|
||||
for (const issue of parsed.error.issues)
|
||||
@@ -22,9 +34,15 @@ export const jsonRecordSchema = <Value extends JsonValue>(
|
||||
return invalid ? z.NEVER : Object.fromEntries(entries);
|
||||
},
|
||||
);
|
||||
// Delegate to Zod's own record projection with the caller's dialect, IO and
|
||||
// reference context; a context-free JSON override would lose those choices.
|
||||
schema._zod.processJSONSchema = (context, json, params) =>
|
||||
projection._zod.processJSONSchema!(context, json, params);
|
||||
return schema;
|
||||
// Zod clones reconstruct from the definition. Install projection in the
|
||||
// constructor so descriptions, metadata and checks retain the same owner.
|
||||
const JsonRecord = z.core.$constructor<typeof schema>(
|
||||
"JsonRecord",
|
||||
(instance, definition) => {
|
||||
z.ZodPipe.init(instance, definition);
|
||||
instance._zod.processJSONSchema = (context, json, params) =>
|
||||
projection._zod.processJSONSchema!(context, json, params);
|
||||
},
|
||||
);
|
||||
return new JsonRecord(schema._zod.def);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { jsonObjectSchema } from "../jsonValue.js";
|
||||
import { jsonRecordSchema } from "../jsonRecord.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
|
||||
const hexSchema = z.string().regex(/^0x[0-9a-f]+$/u);
|
||||
@@ -66,30 +66,11 @@ const environmentNameSchema = z
|
||||
const environmentValueSchema = z
|
||||
.string()
|
||||
.regex(/^[^\x00]*$/u, "Environment values cannot contain NUL");
|
||||
const environmentRecordProjection = z.record(
|
||||
environmentNameSchema,
|
||||
const nativeCallEnvironmentSchema = jsonRecordSchema(
|
||||
environmentValueSchema,
|
||||
environmentNameSchema,
|
||||
);
|
||||
|
||||
// z.record skips __proto__. Own the JSON map before validating entries so every
|
||||
// selected environment name survives input parsing, readback and partial Evidence.
|
||||
const nativeCallEnvironmentSchema = jsonObjectSchema
|
||||
.superRefine((value, context) => {
|
||||
for (const [name, entry] of Object.entries(value)) {
|
||||
for (const parsed of [
|
||||
environmentNameSchema.safeParse(name),
|
||||
environmentValueSchema.safeParse(entry),
|
||||
]) {
|
||||
if (!parsed.success)
|
||||
for (const issue of parsed.error.issues)
|
||||
context.addIssue({ ...issue, path: [name, ...issue.path] });
|
||||
}
|
||||
}
|
||||
})
|
||||
.transform((value) => value as Record<string, string>);
|
||||
nativeCallEnvironmentSchema._zod.toJSONSchema = () =>
|
||||
z.toJSONSchema(environmentRecordProjection, { io: "input" });
|
||||
|
||||
/**
|
||||
* Launch the active Mach-O as an owned process under LLDB and observe calls.
|
||||
* Duration and event bounds keep runtime observation finite.
|
||||
|
||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
import { SESSION_TOOL_CONTRACTS } from "../../contracts/sessionToolContracts.js";
|
||||
import { projectInputIssues } from "../inputIssueProjection.js";
|
||||
import { nativeCallObservationInputSchema } from "../native/nativeCallObservation.js";
|
||||
import {
|
||||
digestProcessCommitment,
|
||||
PROCESS_TERMINAL_CELL_BUDGET,
|
||||
@@ -295,7 +297,9 @@ it.each(["selected-value", 7])(
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success)
|
||||
throw new Error("Expected the unsupported PTY name to be rejected");
|
||||
expect(parsed.error.issues).toContainEqual(
|
||||
expect(
|
||||
projectInputIssues(parsed.error.issues, { ...baseScenario, environment }),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: ["environment", "__proto__"],
|
||||
message: expect.stringContaining("PTY backend cannot preserve"),
|
||||
@@ -303,3 +307,23 @@ it.each(["selected-value", 7])(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps native and PTY environment admission distinct through Standard Schema export", () => {
|
||||
const environment = Object.fromEntries([
|
||||
["__proto__", "selected"],
|
||||
["constructor", "ordinary"],
|
||||
]);
|
||||
const native = nativeCallObservationInputSchema.shape.environment;
|
||||
const parsed = native.parse(environment);
|
||||
expect(Object.hasOwn(parsed, "__proto__")).toBe(true);
|
||||
expect(JSON.stringify(parsed)).toBe(JSON.stringify(environment));
|
||||
const ajv = new Ajv2020({ strict: false });
|
||||
const options = { target: "draft-2020-12" as const };
|
||||
const nativePublished = native["~standard"].jsonSchema.input(options);
|
||||
const ptyPublished =
|
||||
processScenarioSchema.shape.environment["~standard"].jsonSchema.input(
|
||||
options,
|
||||
);
|
||||
expect(ajv.compile(nativePublished)(environment)).toBe(true);
|
||||
expect(ajv.compile(ptyPublished)(environment)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
import { jsonRecordSchema } from "../jsonRecord.js";
|
||||
|
||||
const positiveBudget = z.int().safe().positive();
|
||||
/** Read the explicitly committed output budget without applying scenario defaults. */
|
||||
@@ -59,29 +60,10 @@ const childProcessString = z
|
||||
/^[^\x00]*$/u,
|
||||
"Values passed to operating-system APIs cannot contain NUL",
|
||||
);
|
||||
const environmentRecordSchema = z.record(
|
||||
ptyEnvironmentName,
|
||||
const processEnvironmentSchema = jsonRecordSchema(
|
||||
childProcessString,
|
||||
ptyEnvironmentName,
|
||||
);
|
||||
// z.record intentionally skips this key, so validate its presence before parsing.
|
||||
const processEnvironmentSchema = z
|
||||
.unknown()
|
||||
.superRefine((value, context) => {
|
||||
if (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
Object.hasOwn(value, unsupportedPtyEnvironmentName)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [unsupportedPtyEnvironmentName],
|
||||
message:
|
||||
"The installed PTY backend cannot preserve the __proto__ environment name; pass it through /usr/bin/env instead.",
|
||||
});
|
||||
})
|
||||
.pipe(environmentRecordSchema);
|
||||
processEnvironmentSchema._zod.toJSONSchema = () =>
|
||||
z.toJSONSchema(environmentRecordSchema, { io: "input" });
|
||||
|
||||
const fitsProcessTerminalCellBudget = (
|
||||
columns: number,
|
||||
|
||||
@@ -5,6 +5,28 @@ import {
|
||||
parseGhidraInventoryResult,
|
||||
} from "./GhidraInventoryValues.js";
|
||||
|
||||
it.each(["other", 42, null, {}])(
|
||||
"rejects an invalid own destination with value %j before admitting any batch naming subset",
|
||||
(value) => {
|
||||
const input = {
|
||||
names: Object.fromEntries([
|
||||
["0x1000", "entry"],
|
||||
["__proto__", value],
|
||||
]),
|
||||
};
|
||||
expect(
|
||||
parseGhidraInventoryInput("set_addresses_names", input),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [{ path: ["names", "__proto__"] }],
|
||||
},
|
||||
});
|
||||
expect(Object.keys(input.names)).toEqual(["0x1000", "__proto__"]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["list_procedures", "search_procedures"] as const)(
|
||||
"preserves real bridge symbol provenance in %s",
|
||||
(operation) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { nativeSymbolMetadataSchema } from "../domain/native/nativeSymbol.js";
|
||||
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
|
||||
import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js";
|
||||
import { z } from "zod";
|
||||
import { jsonRecordSchema } from "../domain/jsonRecord.js";
|
||||
import {
|
||||
bytePatternFields,
|
||||
validateBytePattern,
|
||||
@@ -125,7 +126,7 @@ const inputSchemas = {
|
||||
set_addresses_names: z
|
||||
.object({
|
||||
document,
|
||||
names: z.record(explicitAddress, z.string().min(1)),
|
||||
names: jsonRecordSchema(z.string().min(1), explicitAddress),
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
|
||||
@@ -49,3 +49,50 @@ it("retains provider coverage caveats in a composed MCP overview without repeate
|
||||
});
|
||||
expect(calls.length).toBe(new Set(calls).size);
|
||||
});
|
||||
|
||||
it("counts all own inventory dictionary entries in the composed overview", async () => {
|
||||
const procedures = JSON.parse(
|
||||
'{"__proto__":"observed procedure","0x1000":"ordinary procedure"}',
|
||||
);
|
||||
const strings = JSON.parse('{"__proto__":"observed string"}');
|
||||
const before = [JSON.stringify(procedures), JSON.stringify(strings)];
|
||||
const client = await connect({
|
||||
execute: async (operation) => {
|
||||
if (operation === "read_binary_inventory_summary")
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"No native overview",
|
||||
),
|
||||
);
|
||||
const value =
|
||||
operation === "current_document"
|
||||
? "fixture"
|
||||
: operation === "list_procedures"
|
||||
? { procedures }
|
||||
: operation === "list_strings"
|
||||
? strings
|
||||
: [];
|
||||
return ok(
|
||||
createAnalysisExecution(value, {
|
||||
id: "fixture",
|
||||
name: "Fixture",
|
||||
version: "1",
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
const response = await client.callTool({
|
||||
name: "binary_overview",
|
||||
arguments: {},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const evidence = parseEvidence(response.structuredContent);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
document: "fixture",
|
||||
procedure_count: Object.keys(procedures).length,
|
||||
string_count: Object.keys(strings).length,
|
||||
});
|
||||
expect([JSON.stringify(procedures), JSON.stringify(strings)]).toEqual(before);
|
||||
});
|
||||
|
||||
@@ -150,6 +150,40 @@ const connect = async (analysis: AnalysisOperationPort) => {
|
||||
};
|
||||
|
||||
describe("official tool input contracts", () => {
|
||||
it("preserves every batch naming destination and rejects malformed own values before dispatch", async () => {
|
||||
const invocations: Invocation[] = [];
|
||||
const client = await connect({
|
||||
execute: (name, arguments_) => {
|
||||
invocations.push({ name, arguments_ });
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
});
|
||||
const names = JSON.parse('{"0x1000":"entry","__proto__":"other"}');
|
||||
await client.callTool({
|
||||
name: "set_addresses_names",
|
||||
arguments: { names },
|
||||
});
|
||||
expect(invocations).toEqual([
|
||||
{ name: "set_addresses_names", arguments_: { names } },
|
||||
]);
|
||||
expect(JSON.stringify(invocations[0]!.arguments_)).toContain(
|
||||
'"__proto__":"other"',
|
||||
);
|
||||
for (const value of [42, null, {}]) {
|
||||
const result = await client.callTool({
|
||||
name: "set_addresses_names",
|
||||
arguments: {
|
||||
names: Object.fromEntries([
|
||||
["0x1000", "entry"],
|
||||
["__proto__", value],
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
}
|
||||
expect(invocations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects misspelled top-level inputs before dispatch and keeps record keys open", async () => {
|
||||
const invocations: Invocation[] = [];
|
||||
const client = await connect({
|
||||
|
||||
@@ -114,3 +114,30 @@ it("parses explicit per-path expectations without borrowing the production phase
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("retains prototype-named source paths in independent expectations", () => {
|
||||
const input = Object.fromEntries([
|
||||
["__proto__", expected],
|
||||
["constructor", expected],
|
||||
]);
|
||||
const parsed = structuredScopeExpectationsSchema.parse(input);
|
||||
expect(Object.keys(parsed)).toEqual(["__proto__", "constructor"]);
|
||||
expect(Object.hasOwn(parsed, "__proto__")).toBe(true);
|
||||
expect(parsed["__proto__"]).toEqual(expected);
|
||||
});
|
||||
|
||||
it("validates prototype-named expectations and retains the nonempty path policy", () => {
|
||||
for (const entry of [
|
||||
null,
|
||||
{ ...expected, operation: "wrong" },
|
||||
{ ...expected, analysis_status: "wrong" },
|
||||
])
|
||||
expect(
|
||||
structuredScopeExpectationsSchema.safeParse(
|
||||
Object.fromEntries([["__proto__", entry]]),
|
||||
).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
structuredScopeExpectationsSchema.safeParse({ "": expected }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user