mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
Add rea environment-export <manifest.json> --evidence <capture.json> <dir> [--package-tarball <path>] [--name <override>]: it reads and authenticates the manifest and capture pair once and writes an agent environment bundle folder (README with provenance and a rights note, envs/<name> with a Dockerfile that pins this exact rea version or installs a given tarball, the manifest and the Evidence as byte-identical copies, a Python verifier that checks replay through the MCP endpoint, and a smoke task) that the agentenv framework runs with agent-env run. Refuses a non-empty directory and a mismatched pair before writing. Adds the real-agentenv hosted lane, which packs the checkout, exports a bundle from the source-owned capture fixture and runs it on the runner's Docker with a pinned agentenv-framework, wired into the CI planner and its guard tests. Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
183 lines
7.3 KiB
TypeScript
183 lines
7.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { environmentServeSource } from "../../../tests/fixtures/environmentServe.js";
|
|
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
|
import { PACKAGE_METADATA } from "../../generatedPackageMetadata.js";
|
|
import {
|
|
BUNDLE_BASE_IMAGE,
|
|
bundleDockerfile,
|
|
planEnvironmentBundle,
|
|
registryPackageSource,
|
|
} from "./environmentBundle.js";
|
|
|
|
const plan = (options: { name?: string; tarball?: string } = {}) => {
|
|
const planned = planEnvironmentBundle(environmentServeSource(), {
|
|
name: options.name,
|
|
packageSource:
|
|
options.tarball === undefined
|
|
? registryPackageSource()
|
|
: { kind: "tarball", path: options.tarball },
|
|
});
|
|
if (!planned.ok) throw planned.error;
|
|
return planned.value;
|
|
};
|
|
|
|
const file = (planned: ReturnType<typeof plan>, path: string): string => {
|
|
const found = planned.files.find((entry) => entry.path === path);
|
|
if (found === undefined) throw new Error(`Planned file missing: ${path}`);
|
|
return found.content;
|
|
};
|
|
|
|
describe("planEnvironmentBundle", () => {
|
|
it("plans the echo-shaped file set under the manifest name with byte-exact copies beside the Dockerfile", () => {
|
|
const planned = plan();
|
|
expect(planned.name).toBe("grafana-example");
|
|
expect(planned.toolCount).toBe(19);
|
|
expect(planned.files.map((entry) => entry.path)).toEqual([
|
|
"README.md",
|
|
"envs/grafana-example/env.toml",
|
|
"envs/grafana-example/Dockerfile",
|
|
"artifacts/check_grafana-example/verify.py",
|
|
"tasks/smoke.json",
|
|
]);
|
|
expect(planned.copies).toEqual({
|
|
manifest: "envs/grafana-example/manifest.json",
|
|
evidence: "envs/grafana-example/evidence.json",
|
|
tarball: null,
|
|
});
|
|
expect(file(planned, "envs/grafana-example/env.toml")).toBe(
|
|
'environment_name = "grafana-example"\n',
|
|
);
|
|
});
|
|
|
|
it("pins the registry package to this build's exact version in the Dockerfile", () => {
|
|
const dockerfile = file(plan(), "envs/grafana-example/Dockerfile");
|
|
const lines = dockerfile.trimEnd().split("\n");
|
|
expect(lines[0]).toBe(`FROM ${BUNDLE_BASE_IMAGE}`);
|
|
expect(lines).toContain("ENV MCP_HOST=0.0.0.0");
|
|
expect(lines).toContain("ENV MCP_PORT=18765");
|
|
expect(lines).toContain(
|
|
"RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*",
|
|
);
|
|
expect(lines).toContain(
|
|
`RUN npm install -g --ignore-scripts --no-fund --no-audit rea-agents@${PACKAGE_METADATA.version}`,
|
|
);
|
|
expect(lines).toContain("COPY manifest.json evidence.json /env/");
|
|
expect(lines).toContain("USER node");
|
|
expect(lines.at(-1)).toBe(
|
|
'CMD ["rea","environment-serve","/env/manifest.json","--evidence","/env/evidence.json"]',
|
|
);
|
|
expect(dockerfile).not.toContain("rea-agents.tgz");
|
|
expect(dockerfile.indexOf("RUN npm install")).toBeLessThan(
|
|
dockerfile.indexOf("USER node"),
|
|
);
|
|
});
|
|
|
|
it("copies and installs the tarball instead of the registry package when one is given", () => {
|
|
const planned = plan({ tarball: "/packed/rea-agents-1.2.3.tgz" });
|
|
expect(planned.copies.tarball).toBe("envs/grafana-example/rea-agents.tgz");
|
|
const dockerfile = file(planned, "envs/grafana-example/Dockerfile");
|
|
expect(dockerfile).toContain("COPY rea-agents.tgz /tmp/\n");
|
|
expect(dockerfile).toContain(
|
|
"RUN npm install -g --ignore-scripts --no-fund --no-audit /tmp/rea-agents.tgz && rm /tmp/rea-agents.tgz\n",
|
|
);
|
|
expect(dockerfile).not.toContain(`rea-agents@${PACKAGE_METADATA.version}`);
|
|
expect(bundleDockerfile(registryPackageSource())).not.toBe(dockerfile);
|
|
});
|
|
|
|
it("writes a verifier that checks the tool count and the first parameterless GET tool's recorded status", () => {
|
|
const planned = plan();
|
|
// Manifest tools are name-ordered, so the first parameterless GET is the annotations list.
|
|
expect(planned.check).toEqual({
|
|
tool: "get_annotations",
|
|
recordOrdinal: 25,
|
|
status: 200,
|
|
});
|
|
const verifier = file(planned, "artifacts/check_grafana-example/verify.py");
|
|
expect(verifier).toContain("from mcp import ClientSession");
|
|
expect(verifier).toContain(
|
|
"from mcp.client.streamable_http import streamable_http_client",
|
|
);
|
|
expect(verifier).toContain("async def verify(mcp_url):");
|
|
expect(verifier).toContain("TOOL_COUNT = 19");
|
|
expect(verifier).toContain('TOOL = "get_annotations"');
|
|
expect(verifier).toContain("RECORDED_STATUS = 200");
|
|
expect(verifier).toContain("await session.list_tools()");
|
|
expect(verifier).toContain("await session.call_tool(TOOL, {})");
|
|
expect(verifier).toContain(
|
|
'return [{"id": "replay", "result": passed, "description": description}]',
|
|
);
|
|
});
|
|
|
|
it("writes the smoke task deploying the env locally before the replay verifier", () => {
|
|
expect(JSON.parse(file(plan(), "tasks/smoke.json"))).toEqual([
|
|
{
|
|
id: "env",
|
|
type: "deploy_env",
|
|
env_id: "grafana-example",
|
|
sandbox_type: "local",
|
|
},
|
|
{
|
|
id: "replay",
|
|
type: "env_outcome_verifier",
|
|
env_id: "grafana-example",
|
|
file_artifact_id: "check_grafana-example",
|
|
verifier_id: "replay",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("records provenance, replay limits, the run command and the rights note in the README", () => {
|
|
const source = environmentServeSource();
|
|
const readme = file(plan(), "README.md");
|
|
expect(readme).toContain(
|
|
`- Capture Evidence (the manifest's parent): \`${source.evidenceId}\``,
|
|
);
|
|
expect(readme).toContain(
|
|
`- Manifest Evidence: \`${source.manifestEvidenceId}\``,
|
|
);
|
|
expect(readme).toContain(
|
|
`- Manifest view digest: \`${source.manifest.view_digest}\``,
|
|
);
|
|
expect(readme).toContain(
|
|
`- Capture artifact SHA-256: \`${source.captureSha256 ?? ""}\``,
|
|
);
|
|
expect(readme).toContain("- Tools: 19");
|
|
expect(readme).toContain(`rea-agents@${PACKAGE_METADATA.version}`);
|
|
expect(readme).toContain("```sh\nagent-env run .\n```");
|
|
expect(readme).not.toContain("agent-env run grafana-example");
|
|
expect(readme).toContain("Replay is not emulation");
|
|
expect(readme).toContain("belongs to the captured application");
|
|
expect(readme).toContain("Do not publish this bundle without the rights");
|
|
expect(readme).toContain("calls `get_annotations`");
|
|
});
|
|
|
|
it("accepts a kebab-case name override and refuses any other name as invalid input", () => {
|
|
const renamed = plan({ name: "replay-2" });
|
|
expect(renamed.files.map((entry) => entry.path)).toContain(
|
|
"envs/replay-2/Dockerfile",
|
|
);
|
|
expect(file(renamed, "envs/replay-2/env.toml")).toBe(
|
|
'environment_name = "replay-2"\n',
|
|
);
|
|
expect(JSON.parse(file(renamed, "tasks/smoke.json"))[1]).toMatchObject({
|
|
env_id: "replay-2",
|
|
file_artifact_id: "check_replay-2",
|
|
});
|
|
for (const name of ["Grafana", "a_b", "-a", "a--b", "", "a".repeat(65)]) {
|
|
const refused = planEnvironmentBundle(environmentServeSource(), {
|
|
name,
|
|
packageSource: registryPackageSource(),
|
|
});
|
|
if (refused.ok) throw new Error(`Expected ${name} to be refused`);
|
|
expect(projectAnalysisError(refused.error)).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "environment-export",
|
|
issues: [{ path: ["name"], reason: "invalid_format" }],
|
|
},
|
|
});
|
|
}
|
|
});
|
|
});
|