From ee306e5c4e95062e885d3bc579ed1215eb5a0da8 Mon Sep 17 00:00:00 2001 From: Utkarshpandey0001 Date: Sun, 20 Sep 2026 00:06:41 +0530 Subject: [PATCH] fix: include response limit in build cache key --- packages/dynamic-apps-core/src/build.ts | 1 + .../dynamic-apps-core/tests/build.test.ts | 129 +++++++++++++++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/packages/dynamic-apps-core/src/build.ts b/packages/dynamic-apps-core/src/build.ts index 7b30d0510..3640c77ed 100644 --- a/packages/dynamic-apps-core/src/build.ts +++ b/packages/dynamic-apps-core/src/build.ts @@ -360,6 +360,7 @@ export async function buildAppRelease( `manifest@${appBundleManifestVersion}`, "direct@2", `actors@${plan.usesRivetKit ? 1 : 0}`, + `maxResponseBytes@${config.maxResponseBytes}`, "esbuild-wasm@0.27.4", ].join(";"), }); diff --git a/packages/dynamic-apps-core/tests/build.test.ts b/packages/dynamic-apps-core/tests/build.test.ts index ec7b4ffbf..beadc0fee 100644 --- a/packages/dynamic-apps-core/tests/build.test.ts +++ b/packages/dynamic-apps-core/tests/build.test.ts @@ -3,11 +3,21 @@ import { createHash } from "node:crypto"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { packAospkgFromTarBytes } from "@rivet-dev/agentos-toolchain"; import { afterEach, describe, expect, test } from "vitest"; -import { buildAppRelease, readBuildConfig } from "../src/build.js"; -import { DIRECT_ENTRYPOINT, DIRECT_RUNTIME_FORMAT } from "../src/runtime.js"; +import { extractAospkgTextFile } from "../src/artifact.js"; +import { + buildAppRelease, + DEFAULT_MAX_RESPONSE_BYTES, + readBuildConfig, +} from "../src/build.js"; +import { + DIRECT_ENTRYPOINT, + DIRECT_RUNTIME_FORMAT, + directRunnerSource, +} from "../src/runtime.js"; const execFileAsync = promisify(execFile); const temporaryDirectories: string[] = []; @@ -70,16 +80,97 @@ describe("buildAppRelease", () => { cached[0] = original === 0 ? 1 : 0; expect(result.artifact.bytes[0]).toBe(original); }); + + test("keys cached wrappers by the effective response limit", async () => { + const cachedArtifacts = await Promise.all([ + makeArtifact(1024), + makeArtifact(2048), + makeArtifact(DEFAULT_MAX_RESPONSE_BYTES), + ]); + const cache = new Map(); + const cacheReads: string[] = []; + const artifactCache = { + async get(buildId: string) { + cacheReads.push(buildId); + if (!cache.has(buildId)) { + const artifact = cachedArtifacts[cache.size]; + if (!artifact) throw new Error("unexpected cache miss"); + cache.set(buildId, artifact); + } + return cache.get(buildId); + }, + async put() { + throw new Error("pre-populated cache must not write"); + }, + }; + const input = { + appId: "response-limit", + files: { + "package.json": new TextEncoder().encode( + JSON.stringify({ type: "module", main: "index.mjs" }), + ), + "index.mjs": new TextEncoder().encode( + `export default { fetch() { return new Response("x".repeat(1536)) } }`, + ), + }, + }; + + const lower = await buildAppRelease(input, { + config: { maxResponseBytes: 1024 }, + artifactCache, + }); + const higher = await buildAppRelease(input, { + config: { maxResponseBytes: 2048 }, + artifactCache, + }); + const higherAgain = await buildAppRelease(input, { + config: { maxResponseBytes: 2048 }, + artifactCache, + }); + const implicitDefault = await buildAppRelease(input, { artifactCache }); + const explicitDefault = await buildAppRelease(input, { + config: { maxResponseBytes: DEFAULT_MAX_RESPONSE_BYTES }, + artifactCache, + }); + + expect(lower.buildId).not.toBe(higher.buildId); + expect(higherAgain.buildId).toBe(higher.buildId); + expect(higherAgain.artifact.hash).toBe(higher.artifact.hash); + expect(explicitDefault.buildId).toBe(implicitDefault.buildId); + expect(explicitDefault.artifact.hash).toBe(implicitDefault.artifact.hash); + expect(cacheReads).toEqual([ + lower.buildId, + higher.buildId, + higher.buildId, + implicitDefault.buildId, + implicitDefault.buildId, + ]); + expect(cache.size).toBe(3); + await expect(dispatchArtifact(lower.artifact.bytes)).rejects.toThrow( + "Dynamic App response exceeds the configured limit", + ); + const response = await dispatchArtifact(higher.artifact.bytes); + expect(Buffer.from(response.bodyBase64, "base64")).toHaveLength(1536); + }); }); -async function makeArtifact(): Promise { +async function makeArtifact( + maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, +): Promise { const directory = await mkdtemp(join(tmpdir(), "dynamic-apps-core-build-")); temporaryDirectories.push(directory); await mkdir(join(directory, "direct")); await writeFile( join(directory, "direct", "main.mjs"), - `export const dynamicAppMetadata = { format: ${JSON.stringify(DIRECT_RUNTIME_FORMAT)} }; -export async function dispatch() { return { status: 200, headers: [], bodyBase64: "" }; }`, + directRunnerSource({ + entrypoint: "index.mjs", + release: `response-limit-${maxResponseBytes}`, + maxResponseBytes, + }), + ); + await writeFile( + join(directory, "direct", "index.mjs"), + `export default { fetch() { return new Response("x".repeat(1536)) } }`, ); await writeFile( join(directory, "agentos-package.json"), @@ -93,3 +184,31 @@ export async function dispatch() { return { status: 200, headers: [], bodyBase64 ); return new Uint8Array(packAospkgFromTarBytes(await readFile(archive)).bytes); } + +async function dispatchArtifact( + artifact: Uint8Array, +): Promise<{ bodyBase64: string }> { + const directory = await mkdtemp(join(tmpdir(), "dynamic-apps-dispatch-")); + temporaryDirectories.push(directory); + await writeFile( + join(directory, "index.mjs"), + extractAospkgTextFile(artifact, "direct/index.mjs"), + ); + const runnerPath = join(directory, "main.mjs"); + await writeFile( + runnerPath, + extractAospkgTextFile(artifact, "direct/main.mjs"), + ); + const runner = (await import(pathToFileURL(runnerPath).href)) as { + dispatch(input: { + url: string; + method: string; + headers: Array<[string, string]>; + }): Promise<{ bodyBase64: string }>; + }; + return runner.dispatch({ + url: "https://example.test/", + method: "GET", + headers: [], + }); +}