diff --git a/tests/runner-e2e/daytona-image-content.ts b/tests/runner-e2e/daytona-image-content.ts index ab566d7bd7..bb15e2819a 100644 --- a/tests/runner-e2e/daytona-image-content.ts +++ b/tests/runner-e2e/daytona-image-content.ts @@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const repositoryRoot = path.resolve(import.meta.dirname, "../.."); export const DAYTONA_IMAGE_CONTENT_SCHEMA = - "paperclip-daytona-runner-image-content/v3"; + "paperclip-daytona-runner-image-content/v4"; export const DAYTONA_IMAGE_PLATFORM = "linux/amd64"; export const DAYTONA_IMAGE_DOCKERFILE_PATH = "docker/daytona-runner/Dockerfile"; @@ -27,11 +27,30 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [ "packages/paperclip-runner", ] as const; -const ignoredDirectoryPaths = new Set([ +const ignoredGeneratedDirectoryPaths = new Set([ "packages/paperclip-runner/dist", "packages/paperclip-runner/runner/target", ]); +// These paths may be copied into Docker's provider-build context, but they do +// not contribute to the release runnerd binary or the executable/digested +// provider-pack runtime payload. Keep the exclusions narrow and package-scoped: +// unknown non-documentation files remain hashed by default, so a newly +// introduced runtime input fails safe by invalidating the image key. +const ignoredRunnerDevelopmentDirectoryPaths = new Set([ + "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", +]); + +const runnerDocumentationFilePattern = /\.md$/; +const runnerTestFilePattern = /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/; +const runnerRustIntegrationTestPathPattern = + /^packages\/paperclip-runner\/runner\/crates\/[^/]+\/tests(?:\/|$)/; + export interface DaytonaImageContentOptions { repositoryRoot?: string; inputPaths?: readonly string[]; @@ -49,10 +68,20 @@ function normalizedRelativePath(value: string): string { } function shouldIgnore(relativePath: string): boolean { - if (ignoredDirectoryPaths.has(relativePath)) return true; + if (ignoredGeneratedDirectoryPaths.has(relativePath)) return true; return relativePath.split("/").includes("node_modules"); } +function shouldIgnoreRunnerDevelopmentInput(relativePath: string): boolean { + if (!relativePath.startsWith("packages/paperclip-runner/")) return false; + if (ignoredRunnerDevelopmentDirectoryPaths.has(relativePath)) return true; + return ( + runnerDocumentationFilePattern.test(relativePath) || + runnerTestFilePattern.test(relativePath) || + runnerRustIntegrationTestPathPattern.test(relativePath) + ); +} + function updateRecord( hash: ReturnType, kind: string, @@ -160,7 +189,12 @@ async function hashEntry( relativePath: string, ): Promise { const normalizedPath = normalizedRelativePath(relativePath); - if (shouldIgnore(normalizedPath)) return; + if ( + shouldIgnore(normalizedPath) || + shouldIgnoreRunnerDevelopmentInput(normalizedPath) + ) { + return; + } const absolutePath = path.resolve(root, relativePath); const relativeFromRoot = path.relative(root, absolutePath); diff --git a/tests/runner-e2e/daytona-image.test.ts b/tests/runner-e2e/daytona-image.test.ts index 19211d331d..e7c17c3bfd 100644 --- a/tests/runner-e2e/daytona-image.test.ts +++ b/tests/runner-e2e/daytona-image.test.ts @@ -177,6 +177,102 @@ describe("runner E2E Daytona image contract", () => { } }); + 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, "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, "runner/crates/runner-core/tests/recovery.rs"), + "// first Rust integration test\n", + ); + + const baseline = await computeDaytonaImageContentId(options); + 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, "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",