* feat(environment): render more read tools from the state and describe responses Keep main's environment serving and fold in coverage for non-entity reads, response summaries, and MCP tool annotations. Squashed rebase of #5352 onto main. * fix(environment): keep rendered documents and quoted samples inside their budgets A recorded body of exponential numbers can render past 256 KiB and turn a working read into a state size error, so that document stays a recorded replay. Description samples are measured after JSON quoting, and the replay-fallback catalog check uses a tool count the longer response descriptions still separate. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(environment): pin the replay card this branch serves The replay-only card test pinned main's 11,131-byte card. This branch's descriptions name each tool's recorded responses, so the card it serves is 12,818 bytes; the pin and its name follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { environmentServeSource } from "../../../tests/fixtures/environmentServe.js";
|
|
import {
|
|
environmentCard,
|
|
environmentToolDefinitions,
|
|
ENVIRONMENT_CATALOG_LIMIT_BYTES,
|
|
} from "../../application/environment/environmentTools.js";
|
|
import { serveEnvironment } from "./serveEnvironment.js";
|
|
|
|
it("refuses a complete oversized discovery before trying to bind", async () => {
|
|
const source = environmentServeSource();
|
|
const tool = source.manifest.tools[0]!;
|
|
const manifest = {
|
|
...source.manifest,
|
|
tools: [
|
|
{
|
|
...tool,
|
|
query_parameter_names: Array.from(
|
|
{ length: 10000 },
|
|
(_, index) => `q_${index}`,
|
|
),
|
|
},
|
|
],
|
|
};
|
|
const result = await serveEnvironment({
|
|
source: { ...source, manifest },
|
|
host: "invalid.rea.invalid",
|
|
port: 0,
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "capacity",
|
|
retryable: false,
|
|
reportedLimits: { max_catalog_bytes: ENVIRONMENT_CATALOG_LIMIT_BYTES },
|
|
},
|
|
});
|
|
expect(manifest.tools[0]?.query_parameter_names).toHaveLength(10000);
|
|
});
|
|
|
|
it("charges environment identity, aggregate tools and escaped text before producing a card", () => {
|
|
const source = environmentServeSource();
|
|
const tools = Array.from({ length: 1000 }, (_, index) => ({
|
|
...source.manifest.tools[0]!,
|
|
name: `tool_${index}`,
|
|
params: [],
|
|
query_parameter_names: [],
|
|
}));
|
|
expect(
|
|
environmentToolDefinitions({
|
|
...source,
|
|
manifest: { ...source.manifest, tools },
|
|
}).ok,
|
|
).toBe(false);
|
|
expect(
|
|
environmentToolDefinitions(
|
|
source,
|
|
"x".repeat(ENVIRONMENT_CATALOG_LIMIT_BYTES),
|
|
).ok,
|
|
).toBe(false);
|
|
const normal = environmentToolDefinitions(source);
|
|
expect(normal.ok).toBe(true);
|
|
if (!normal.ok) throw normal.error;
|
|
expect(
|
|
Buffer.byteLength(
|
|
JSON.stringify(
|
|
normal.value.map(({ tool: _, ...definition }) => definition),
|
|
),
|
|
),
|
|
).toBeLessThan(ENVIRONMENT_CATALOG_LIMIT_BYTES);
|
|
});
|
|
|
|
it("reserves the state card only when the state data plane is served", () => {
|
|
const source = environmentServeSource();
|
|
const manifest = { ...source.manifest, tools: [] };
|
|
const admits = (length: number, dataPlane: "none" | "state") =>
|
|
environmentToolDefinitions({ ...source, manifest }, "x".repeat(length), {
|
|
dataPlane,
|
|
}).ok;
|
|
// With no tools, the name and the card are the whole reservation: six bytes per name unit plus quotes.
|
|
const longest = (dataPlane: "none" | "state") =>
|
|
Math.floor(
|
|
(ENVIRONMENT_CATALOG_LIMIT_BYTES -
|
|
2 -
|
|
JSON.stringify(environmentCard("", [], dataPlane)).length) /
|
|
6,
|
|
);
|
|
const stateless = longest("none");
|
|
expect(admits(stateless, "none")).toBe(true);
|
|
expect(admits(stateless + 1, "none")).toBe(false);
|
|
expect(longest("state")).toBeLessThan(stateless);
|
|
expect(admits(stateless, "state")).toBe(false);
|
|
expect(admits(longest("state"), "state")).toBe(true);
|
|
expect(admits(longest("state") + 1, "state")).toBe(false);
|
|
});
|