mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 15:35:12 +01:00
Add the Kimi lifecycle and recovery qualification harness. Preserve original grades and cleanup evidence, strict native ownership audits, bounded startup and cleanup, and early rejection of unsupported local fault execution before credentials or resources. Co-Authored-By: Paperclip <noreply@paperclip.ing>
619 lines
27 KiB
TypeScript
619 lines
27 KiB
TypeScript
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
computeDaytonaImageContentId,
|
|
DAYTONA_IMAGE_DOCKERFILE_PATH,
|
|
DAYTONA_IMAGE_INPUT_PATHS,
|
|
extractDaytonaBaseImages,
|
|
extractDaytonaDockerfileFrontendDigest,
|
|
} from "./daytona-image-content.js";
|
|
|
|
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
|
function lastDockerignoreRule(dockerignore: string, fixturePath: string) {
|
|
return dockerignore.split(/\r?\n/).filter(rule => rule && !rule.startsWith("#")).filter(rule => {
|
|
const pattern = rule.startsWith("!") ? rule.slice(1) : rule;
|
|
return path.matchesGlob(fixturePath, pattern) || fixturePath.startsWith(`${pattern}/`);
|
|
}).at(-1);
|
|
}
|
|
|
|
describe("runner E2E Daytona image contract", () => {
|
|
// ACPX generation recomputes these release attestation inputs in the image build.
|
|
const attestationInputs = [
|
|
"acpx-profiles.json",
|
|
"scripts/acpx-release-attestations.mjs",
|
|
"scripts/generate-acpx-profiles.mjs",
|
|
"src/drivers/acpx/copilot-closure-pins.ts",
|
|
"src/drivers/acpx/pi-closure-pins.ts",
|
|
"src/drivers/acpx/pi-node-pins.ts",
|
|
"src/drivers/acpx/kimi-closure-pins.ts",
|
|
"src/providers/grok/launcher.cjs",
|
|
].map(input => `packages/paperclip-runner/${input}`);
|
|
|
|
it("retains the ACPX release attestation inputs in the Docker context", async () => {
|
|
const dockerignore = await readFile(path.join(repositoryRoot, ".dockerignore"), "utf8");
|
|
for (const input of attestationInputs) {
|
|
await expect(readFile(path.join(repositoryRoot, input), "utf8")).resolves.toBeTruthy();
|
|
// Docker's last matching rule wins, including the parent-directory exclusion.
|
|
expect(lastDockerignoreRule(dockerignore, input)?.startsWith("!") ?? true, input).toBe(true);
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS.some(inputPath => input === inputPath || input.startsWith(`${inputPath}/`)), input).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("invalidates the image content ID when a release attestation input changes", async () => {
|
|
const manifestPath = "packages/paperclip-runner/acpx-profiles.json";
|
|
const root = await mkdtemp(path.join(tmpdir(), "paperclip-daytona-attestation-"));
|
|
const options = {
|
|
repositoryRoot: root,
|
|
inputPaths: DAYTONA_IMAGE_INPUT_PATHS.filter(inputPath => inputPath === manifestPath),
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"c".repeat(64)}`,
|
|
} as const;
|
|
try {
|
|
expect(options.inputPaths).toEqual([manifestPath]);
|
|
await mkdir(path.dirname(path.join(root, manifestPath)), { recursive: true });
|
|
await writeFile(path.join(root, manifestPath), '{"commandDigest":"before"}\n');
|
|
const before = await computeDaytonaImageContentId(options);
|
|
expect(await computeDaytonaImageContentId(options)).toBe(before);
|
|
const withoutManifest = { ...options, inputPaths: [] };
|
|
const unrelatedBefore = await computeDaytonaImageContentId(withoutManifest);
|
|
await writeFile(path.join(root, manifestPath), '{"commandDigest":"after"}\n');
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(before);
|
|
expect(await computeDaytonaImageContentId(withoutManifest)).toBe(unrelatedBefore);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("binds each Antigravity input consumed by the merged provider entrypoint builder", async () => {
|
|
const required = [
|
|
"packages/paperclip-runner/antigravity-distributions.json",
|
|
"packages/paperclip-runner/scripts/provision-antigravity.mjs",
|
|
"packages/paperclip-runner/scripts/antigravity-provisioner-layout.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-antigravity-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/patch-antigravity-runtime.mjs",
|
|
"packages/paperclip-runner/scripts/patch-antigravity-runtime.admission.py",
|
|
];
|
|
const root = await mkdtemp(path.join(tmpdir(), "paperclip-daytona-builder-inputs-"));
|
|
const options = {
|
|
repositoryRoot: root,
|
|
inputPaths: DAYTONA_IMAGE_INPUT_PATHS.filter(input => required.includes(input)),
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"c".repeat(64)}`,
|
|
} as const;
|
|
try {
|
|
for (const input of required) {
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).toContain(input);
|
|
await mkdir(path.dirname(path.join(root, input)), { recursive: true });
|
|
await writeFile(path.join(root, input), await readFile(path.join(repositoryRoot, input)));
|
|
}
|
|
const baseline = await computeDaytonaImageContentId(options);
|
|
for (const input of required) {
|
|
const original = await readFile(path.join(root, input));
|
|
await writeFile(path.join(root, input), Buffer.concat([original, Buffer.from("\nchanged-build-input\n")]));
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
|
await rm(path.join(root, input));
|
|
await expect(computeDaytonaImageContentId(options)).rejects.toThrow();
|
|
await writeFile(path.join(root, input), original);
|
|
expect(await computeDaytonaImageContentId(options)).toBe(baseline);
|
|
}
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps the qualified native Grok binary separate from the legacy command", async () => {
|
|
const [dockerfile, packBuilder, runnerPackage] = await Promise.all([
|
|
readFile(path.join(repositoryRoot, "docker/daytona-runner/Dockerfile"), "utf8"),
|
|
readFile(path.join(repositoryRoot, "packages/paperclip-runner/scripts/build-provider-pack.mjs"), "utf8"),
|
|
readFile(path.join(repositoryRoot, "packages/paperclip-runner/package.json"), "utf8"),
|
|
]);
|
|
expect(dockerfile).toMatch(/@xai-official\/grok@\d+\.\d+\.\d+/);
|
|
expect(dockerfile).not.toMatch(/for cli in[^;]*\bgrok\b/);
|
|
expect(packBuilder).not.toMatch(/writePortable\w+Shim\("grok"/);
|
|
expect(JSON.parse(runnerPackage).dependencies).not.toHaveProperty("@paperclipai/grok-acp");
|
|
expect(packBuilder).toContain('path: "dist/providers/grok/launcher.cjs"');
|
|
expect(dockerfile).toContain("scripts/provision-grok.mjs");
|
|
expect(dockerfile).toContain("/opt/paperclip/providers/grok/1.0.13/grok");
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).toContain("packages/paperclip-runner/scripts/provision-grok.mjs");
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).not.toContain("packages/grok-acp/package.json");
|
|
});
|
|
|
|
it("builds runnerd and the provider pack and verifies every required transport", async () => {
|
|
const [dockerfile, dockerignore, workflow] = await Promise.all([
|
|
readFile(
|
|
path.join(repositoryRoot, "docker/daytona-runner/Dockerfile"),
|
|
"utf8",
|
|
),
|
|
readFile(path.join(repositoryRoot, ".dockerignore"), "utf8"),
|
|
readFile(
|
|
path.join(
|
|
repositoryRoot,
|
|
".github/workflows/runner-full-stack-e2e.yml",
|
|
),
|
|
"utf8",
|
|
),
|
|
]);
|
|
const normalizedDockerfile = dockerfile.replace(/\\\r?\n\s*/g, " ");
|
|
const daytonaImageJob = workflow.match(/^ daytona_image:\n[\s\S]*?(?=^ \w+:)/m)?.[0];
|
|
expect(daytonaImageJob).toBeDefined();
|
|
expect(dockerfile).toContain("--bin paperclip-runnerd");
|
|
expect(dockerfile).toContain("build-provider-pack.mjs /provider-pack");
|
|
expect(normalizedDockerfile).not.toContain(
|
|
"COPY packages/paperclip-eval-kernel ./packages/paperclip-eval-kernel",
|
|
);
|
|
expect(normalizedDockerfile).not.toContain(
|
|
"COPY packages/paperclip-runner ./packages/paperclip-runner",
|
|
);
|
|
expect(dockerfile).toContain(
|
|
"COPY packages ./packages",
|
|
);
|
|
expect(dockerfile).toContain(
|
|
"/opt/paperclip-runner/provider-pack/provider-pack.json",
|
|
);
|
|
expect(dockerfile).toContain(
|
|
"${PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT}/node_modules/.bin",
|
|
);
|
|
for (const command of ["acpx", "claude-agent-acp", "codex-acp"]) {
|
|
expect(dockerfile).toContain(command);
|
|
}
|
|
for (const transport of ["dial_ws_loopback", "dial_wss", "listen_ws"]) {
|
|
expect(dockerfile).toContain(transport);
|
|
}
|
|
expect(dockerfile).toContain(
|
|
'metadata="$(paperclip-runnerd --build-metadata)"',
|
|
);
|
|
expect(dockerfile).toContain("provider-pack.json");
|
|
expect(dockerfile).toContain("io.paperclip.runner.content-id");
|
|
expect(dockerfile).toContain("org.opencontainers.image.revision");
|
|
expect(extractDaytonaDockerfileFrontendDigest(dockerfile)).toBe(
|
|
"sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e",
|
|
);
|
|
expect(extractDaytonaBaseImages(dockerfile)).toEqual([
|
|
"rust:1.97-bookworm@sha256:408fe88047cef61a2087653b0c5255fa51c0f2d6d94ddedd7a2562a9b91a46f6",
|
|
"node:24.21.0-bookworm@sha256:5a750d3be5e5c80275f8c9a5367c3aed99c2875656590c8d0701c7ee687f5f0a",
|
|
"daytonaio/sandbox:0.8.0@sha256:eadf88e4391072b7ad4bed27d9cadfc9fe9d8ed375d9219d34c2ccb518f213e3",
|
|
]);
|
|
expect(dockerignore).toContain("**/node_modules");
|
|
expect(dockerignore).toContain("packages/paperclip-runner/dist");
|
|
expect(dockerignore).toContain("packages/paperclip-runner/runner/target");
|
|
expect(await readFile(path.join(repositoryRoot, ".github/docker-context-checks.Dockerfile"), "utf8")).toContain("node packages/paperclip-runner/scripts/generate-acpx-profiles.mjs --check");
|
|
expect(lastDockerignoreRule(dockerignore, "packages/paperclip-runner/test-fixtures/pi-acp/private-live-session.json")?.startsWith("!")).toBe(false);
|
|
for (const developmentOnlyInput of [
|
|
"packages/paperclip-runner/devtools",
|
|
"packages/paperclip-runner/docs",
|
|
"packages/paperclip-runner/examples",
|
|
"packages/paperclip-runner/test",
|
|
"packages/paperclip-runner/test-fixtures",
|
|
"packages/paperclip-runner/test-support",
|
|
"packages/paperclip-runner/**/*.md",
|
|
"packages/paperclip-runner/**/*.test.ts",
|
|
"packages/paperclip-runner/runner/crates/*/tests",
|
|
"packages/paperclip-runner/scripts/*-smoke.mjs",
|
|
]) {
|
|
expect(dockerignore).toContain(developmentOnlyInput);
|
|
}
|
|
expect(workflow).toContain("--platform linux/amd64");
|
|
expect(workflow).toContain(
|
|
"Compute Daytona image content ID with pinned bases",
|
|
);
|
|
expect(workflow).toContain(
|
|
"e2e-content-${{ needs.catalog.outputs.daytona_image_content_id }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
'--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}"',
|
|
);
|
|
expect(daytonaImageJob).toContain(
|
|
"TARGET_LOCK_SHA256: ${{ needs.target_lock.outputs.lock_sha256 }}",
|
|
);
|
|
expect(daytonaImageJob).toContain(
|
|
'[[ "$TARGET_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]',
|
|
);
|
|
expect(daytonaImageJob).toContain(
|
|
'--build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}"',
|
|
);
|
|
expect(
|
|
daytonaImageJob!.indexOf('[[ "$TARGET_LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]]'),
|
|
).toBeLessThan(
|
|
daytonaImageJob!.indexOf(
|
|
'--build-arg "PAPERCLIP_RUNNER_LOCK_SHA256=${TARGET_LOCK_SHA256}"',
|
|
),
|
|
);
|
|
expect(workflow).toContain(
|
|
"IMAGE_CACHE: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-buildcache-amd64",
|
|
);
|
|
expect(workflow).toContain(
|
|
'--cache-from "type=registry,ref=${IMAGE_CACHE}"',
|
|
);
|
|
expect(workflow).toContain(
|
|
'--cache-to "type=registry,ref=${IMAGE_CACHE},mode=max"',
|
|
);
|
|
expect(workflow).toContain(
|
|
'if [ "$TARGET_REF" = "refs/heads/$DEFAULT_BRANCH" ]; then',
|
|
);
|
|
expect(workflow).not.toContain("e2e-git-${{ github.sha }}");
|
|
expect(workflow).toContain("cosign sign --yes");
|
|
expect(workflow).toContain("docker logout ghcr.io");
|
|
expect(workflow).toContain(`docker buildx imagetools inspect "$immutable"`);
|
|
expect(workflow).toContain(`--format '{{json .Image}}'`);
|
|
expect(workflow).not.toContain(`docker --config "$anonymous_config" pull`);
|
|
// Remote Daytona manifests must use registry inspection. Local oracle
|
|
// images in the test job can still use the Docker daemon.
|
|
expect(daytonaImageJob).not.toContain("docker image inspect");
|
|
expect(workflow).not.toContain("docker buildx prune --all --force");
|
|
expect(workflow).not.toContain("docker system prune --all --force");
|
|
expect(workflow).toContain('.architecture == "amd64"');
|
|
expect(workflow).toContain('.os == "linux"');
|
|
expect(workflow).toContain('.config.User == "daytona"');
|
|
expect(workflow).toContain("PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=");
|
|
expect(workflow).toContain(
|
|
"node packages/paperclip-runner/scripts/build-provider-pack.mjs packages/paperclip-runner/provider-pack",
|
|
);
|
|
expect(workflow).toContain(
|
|
"PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH: ${{ github.workspace }}/packages/paperclip-runner/provider-pack",
|
|
);
|
|
expect(workflow).toContain(
|
|
"PAPERCLIP_RUNNER_SOURCE_REVISION: ${{ needs.daytona_image.outputs.source_revision }}",
|
|
);
|
|
expect(workflow.indexOf("cosign verify")).toBeLessThan(
|
|
workflow.indexOf("docker logout ghcr.io"),
|
|
);
|
|
expect(workflow.indexOf("docker logout ghcr.io")).toBeLessThan(
|
|
workflow.indexOf(`--format '{{json .Image}}'`),
|
|
);
|
|
const providerInstall = dockerfile.indexOf(
|
|
"pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...'",
|
|
);
|
|
const runnerSourceCopy = dockerfile.indexOf(
|
|
"COPY packages ./packages",
|
|
);
|
|
const providerRevisionArg = dockerfile.indexOf(
|
|
"ARG PAPERCLIP_RUNNER_SOURCE_REVISION",
|
|
);
|
|
const cliInstall = dockerfile.indexOf("npm install -g");
|
|
const finalMetadataArgs = dockerfile.lastIndexOf(
|
|
"ARG PAPERCLIP_RUNNER_CONTENT_ID",
|
|
);
|
|
expect(providerInstall).toBeGreaterThan(0);
|
|
expect(runnerSourceCopy).toBeGreaterThan(0);
|
|
expect(runnerSourceCopy).toBeLessThan(providerInstall);
|
|
expect(providerInstall).toBeLessThan(providerRevisionArg);
|
|
expect(cliInstall).toBeGreaterThan(0);
|
|
expect(cliInstall).toBeLessThan(finalMetadataArgs);
|
|
});
|
|
|
|
it("hashes the audited image dependency closure rather than the repository revision", async () => {
|
|
for (const requiredPath of [
|
|
".dockerignore",
|
|
"docker/daytona-runner/Dockerfile",
|
|
"pnpm-lock.yaml",
|
|
"patches",
|
|
"packages/paperclip-eval-kernel/src",
|
|
"packages/paperclip-runner/package.json",
|
|
"packages/paperclip-runner/scripts/candidate-provider-pack.mjs",
|
|
"packages/paperclip-runner/scripts/build-copilot-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/copilot-inner-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/acpx-release-attestations.mjs",
|
|
"packages/paperclip-runner/scripts/generate-acpx-profiles.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-copilot-binary.mjs",
|
|
"packages/paperclip-runner/scripts/install-copilot-assets.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-cursor-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/cursor-runtime-patch.mjs",
|
|
"packages/paperclip-runner/cursor-distributions.json",
|
|
"packages/paperclip-runner/runner/crates",
|
|
"packages/paperclip-runner/scripts/materialize-pi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/pi-distribution",
|
|
"packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/patch-kimi-runtime.mjs",
|
|
"packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs",
|
|
"packages/paperclip-runner/scripts/provision-kimi.mjs",
|
|
"packages/paperclip-runner/scripts/kimi-distribution",
|
|
"packages/paperclip-runner/src",
|
|
]) {
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).toContain(requiredPath);
|
|
}
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).not.toContain(
|
|
"packages/paperclip-eval-kernel",
|
|
);
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS).not.toContain(
|
|
"packages/paperclip-runner",
|
|
);
|
|
expect(DAYTONA_IMAGE_DOCKERFILE_PATH).toBe(
|
|
"docker/daytona-runner/Dockerfile",
|
|
);
|
|
|
|
const contentId = await computeDaytonaImageContentId();
|
|
expect(contentId).toMatch(/^[0-9a-f]{64}$/);
|
|
});
|
|
|
|
it("changes for runtime source, package, lockfile, Dockerfile, frontend, base, or platform inputs", async () => {
|
|
const root = await mkdtemp(
|
|
path.join(tmpdir(), "paperclip-daytona-image-id-"),
|
|
);
|
|
const inputPaths = [
|
|
"docker/daytona-runner/Dockerfile",
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"packages/paperclip-runner/package.json",
|
|
"packages/paperclip-runner/src",
|
|
"packages/paperclip-runner/runner/crates",
|
|
"packages/paperclip-runner/scripts/materialize-pi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/pi-distribution",
|
|
] as const;
|
|
const options = {
|
|
repositoryRoot: root,
|
|
inputPaths,
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"c".repeat(64)}`,
|
|
} as const;
|
|
try {
|
|
await mkdir(path.join(root, "docker/daytona-runner"), {
|
|
recursive: true,
|
|
});
|
|
await mkdir(path.join(root, "packages/paperclip-runner/src"), {
|
|
recursive: true,
|
|
});
|
|
await mkdir(
|
|
path.join(
|
|
root,
|
|
"packages/paperclip-runner/runner/crates/runner-core/src",
|
|
),
|
|
{ recursive: true },
|
|
);
|
|
await writeFile(
|
|
path.join(root, "docker/daytona-runner/Dockerfile"),
|
|
"FROM pinned\n",
|
|
);
|
|
await writeFile(path.join(root, "package.json"), '{"private":true}\n');
|
|
await writeFile(
|
|
path.join(root, "pnpm-lock.yaml"),
|
|
"lockfileVersion: 9\n",
|
|
);
|
|
await writeFile(
|
|
path.join(root, "packages/paperclip-runner/package.json"),
|
|
'{"name":"@paperclipai/paperclip-runner"}\n',
|
|
);
|
|
await writeFile(
|
|
path.join(root, "packages/paperclip-runner/src/runner.ts"),
|
|
"version one\n",
|
|
);
|
|
await writeFile(
|
|
path.join(
|
|
root,
|
|
"packages/paperclip-runner/runner/crates/runner-core/src/lib.rs",
|
|
),
|
|
'pub const VERSION: &str = "one";\n',
|
|
);
|
|
await mkdir(path.join(root, "packages/paperclip-runner/scripts/pi-distribution"), { recursive: true });
|
|
for (const relativePath of [
|
|
"packages/paperclip-runner/scripts/materialize-pi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/pi-distribution/package.json",
|
|
"packages/paperclip-runner/scripts/pi-distribution/package-lock.json",
|
|
]) await writeFile(path.join(root, relativePath), "version one\n");
|
|
const baseline = await computeDaytonaImageContentId(options);
|
|
const candidate = await computeDaytonaImageContentId({ ...options, candidateProviders: ["pi"] });
|
|
expect(candidate).not.toBe(baseline);
|
|
expect(await computeDaytonaImageContentId({ ...options, candidateProviders: ["kimi"] })).not.toBe(baseline);
|
|
expect(await computeDaytonaImageContentId({ ...options, candidateProviders: ["copilot", "pi"] }))
|
|
.toBe(await computeDaytonaImageContentId({ ...options, candidateProviders: ["pi", "copilot"] }));
|
|
await expect(computeDaytonaImageContentId({ ...options, candidateProviders: ["pi", "pi"] }))
|
|
.rejects.toThrow("distinct known");
|
|
await expect(computeDaytonaImageContentId({ ...options, candidateProviders: ["unknown"] }))
|
|
.rejects.toThrow("distinct known");
|
|
expect(
|
|
await computeDaytonaImageContentId({
|
|
...options,
|
|
baseImages: [`example.test/base:1@sha256:${"b".repeat(64)}`],
|
|
}),
|
|
).not.toBe(baseline);
|
|
expect(
|
|
await computeDaytonaImageContentId({
|
|
...options,
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"d".repeat(64)}`,
|
|
}),
|
|
).not.toBe(baseline);
|
|
|
|
await writeFile(
|
|
path.join(root, "unrelated.txt"),
|
|
"does not enter the image\n",
|
|
);
|
|
expect(await computeDaytonaImageContentId(options)).toBe(baseline);
|
|
|
|
for (const relativePath of [
|
|
"docker/daytona-runner/Dockerfile",
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"packages/paperclip-runner/package.json",
|
|
"packages/paperclip-runner/src/runner.ts",
|
|
"packages/paperclip-runner/scripts/materialize-pi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/pi-distribution/package.json",
|
|
"packages/paperclip-runner/scripts/pi-distribution/package-lock.json",
|
|
"packages/paperclip-runner/runner/crates/runner-core/src/lib.rs",
|
|
]) {
|
|
const absolutePath = path.join(root, relativePath);
|
|
const original = await readFile(absolutePath, "utf8");
|
|
await writeFile(absolutePath, `${original}changed\n`);
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
|
await writeFile(absolutePath, original);
|
|
}
|
|
expect(
|
|
await computeDaytonaImageContentId({
|
|
...options,
|
|
platform: "linux/arm64",
|
|
}),
|
|
).not.toBe(baseline);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
"packages/paperclip-runner/scripts/cursor-runtime-patch.mjs",
|
|
"packages/paperclip-runner/scripts/provider-pack-executable-shims.mjs",
|
|
"packages/paperclip-runner/scripts/materialize-kimi-distribution.mjs",
|
|
"packages/paperclip-runner/scripts/patch-kimi-runtime.mjs",
|
|
"packages/paperclip-runner/scripts/kimi-instructions-runtime.mjs",
|
|
"packages/paperclip-runner/scripts/provision-kimi.mjs",
|
|
"packages/paperclip-runner/scripts/kimi-distribution/package.json",
|
|
"packages/paperclip-runner/scripts/kimi-distribution/package-lock.json",
|
|
])("invalidates the image when %s changes", async (patchPath) => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "paperclip-provider-image-id-"));
|
|
expect(DAYTONA_IMAGE_INPUT_PATHS.some(inputPath => patchPath === inputPath || patchPath.startsWith(`${inputPath}/`))).toBe(true);
|
|
const options = {
|
|
repositoryRoot: root,
|
|
inputPaths: [patchPath],
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"c".repeat(64)}`,
|
|
candidateProviders: ["cursor"],
|
|
} as const;
|
|
try {
|
|
await mkdir(path.dirname(path.join(root, patchPath)), { recursive: true });
|
|
await writeFile(path.join(root, patchPath), "export const policy = 'one';\n");
|
|
const baseline = await computeDaytonaImageContentId(options);
|
|
await writeFile(path.join(root, patchPath), "export const policy = 'two';\n");
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("reuses the image for runner-only tests and documentation", async () => {
|
|
const root = await mkdtemp(
|
|
path.join(tmpdir(), "paperclip-daytona-runner-development-inputs-"),
|
|
);
|
|
const options = {
|
|
repositoryRoot: root,
|
|
inputPaths: ["packages/paperclip-runner"],
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: `sha256:${"c".repeat(64)}`,
|
|
} as const;
|
|
try {
|
|
const runnerRoot = path.join(root, "packages/paperclip-runner");
|
|
await mkdir(path.join(runnerRoot, "src/live"), { recursive: true });
|
|
await mkdir(path.join(runnerRoot, "docs"), { recursive: true });
|
|
await mkdir(path.join(runnerRoot, "spec"), { recursive: true });
|
|
await mkdir(path.join(runnerRoot, "scripts"), { recursive: true });
|
|
await mkdir(path.join(runnerRoot, "test-fixtures"), { recursive: true });
|
|
await mkdir(path.join(runnerRoot, "runner/crates/runner-core/src"), {
|
|
recursive: true,
|
|
});
|
|
await mkdir(path.join(runnerRoot, "runner/crates/runner-core/tests"), {
|
|
recursive: true,
|
|
});
|
|
await writeFile(
|
|
path.join(runnerRoot, "src/live/transport.ts"),
|
|
"export const runtime = 'one';\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "runner/crates/runner-core/src/lib.rs"),
|
|
'pub const RUNTIME: &str = "one";\n',
|
|
);
|
|
await writeFile(path.join(runnerRoot, "README.md"), "first readme\n");
|
|
await writeFile(
|
|
path.join(runnerRoot, "docs/local-runner.md"),
|
|
"first documentation\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "spec/architecture.md"),
|
|
"first architecture note\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "src/live/transport.test.ts"),
|
|
"first TypeScript test\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "test-fixtures/provider.json"),
|
|
'{"fixture":"one"}\n',
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "scripts/capability-clean-room-smoke.mjs"),
|
|
"first smoke probe\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "runner/crates/runner-core/tests/recovery.rs"),
|
|
"// first Rust integration test\n",
|
|
);
|
|
|
|
const baseline = await computeDaytonaImageContentId(options);
|
|
await mkdir(path.join(runnerRoot, "src/new-test-only-directory"));
|
|
await writeFile(
|
|
path.join(
|
|
runnerRoot,
|
|
"src/new-test-only-directory/transport-edge.test.ts",
|
|
),
|
|
"new TypeScript test\n",
|
|
);
|
|
await writeFile(path.join(runnerRoot, "README.md"), "second readme\n");
|
|
await writeFile(
|
|
path.join(runnerRoot, "docs/local-runner.md"),
|
|
"second documentation\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "spec/architecture.md"),
|
|
"second architecture note\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "src/live/transport.test.ts"),
|
|
"second TypeScript test\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "test-fixtures/provider.json"),
|
|
'{"fixture":"two"}\n',
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "scripts/capability-clean-room-smoke.mjs"),
|
|
"second smoke probe\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "runner/crates/runner-core/tests/recovery.rs"),
|
|
"// second Rust integration test\n",
|
|
);
|
|
expect(await computeDaytonaImageContentId(options)).toBe(baseline);
|
|
|
|
await writeFile(
|
|
path.join(runnerRoot, "src/live/transport.ts"),
|
|
"export const runtime = 'two';\n",
|
|
);
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
|
|
|
await writeFile(
|
|
path.join(runnerRoot, "src/live/transport.ts"),
|
|
"export const runtime = 'one';\n",
|
|
);
|
|
await writeFile(
|
|
path.join(runnerRoot, "runner/crates/runner-core/src/lib.rs"),
|
|
'pub const RUNTIME: &str = "two";\n',
|
|
);
|
|
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("rejects mutable Docker base references", () => {
|
|
expect(() => extractDaytonaBaseImages("FROM node:24-bookworm\n")).toThrow(
|
|
"must use an immutable sha256 digest",
|
|
);
|
|
});
|
|
|
|
it("rejects a mutable or missing Dockerfile syntax frontend", async () => {
|
|
expect(() =>
|
|
extractDaytonaDockerfileFrontendDigest(
|
|
"# syntax=docker/dockerfile:1.7\nFROM scratch\n",
|
|
),
|
|
).toThrow("must pin its syntax frontend");
|
|
expect(() =>
|
|
extractDaytonaDockerfileFrontendDigest("FROM scratch\n"),
|
|
).toThrow("must pin its syntax frontend");
|
|
await expect(
|
|
computeDaytonaImageContentId({
|
|
inputPaths: [],
|
|
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
|
frontendDigest: "sha256:mutable",
|
|
}),
|
|
).rejects.toThrow("must use an immutable sha256 digest");
|
|
});
|
|
});
|