From 9c155d031079d2344112a127b4f023ebd16f563f Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 29 Apr 2026 10:22:40 -0400 Subject: [PATCH] fix: remove CONFIG_DIR exclusion from zip stage to preserve dependency agentcore/ packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #844 correctly removed the flat name-based agentcore exclusion and threaded rootDir through copySourceTree, but the same CONFIG_DIR check remained in collectFiles/collectFilesSync (the zip stage). Since the zip stage operates on the staging directory — not the project root — the check incorrectly stripped any top-level agentcore/ Python package installed by uv (e.g., langgraph_checkpoint_aws/agentcore/) from the deployment artifact, causing ModuleNotFoundError at runtime. The CONFIG_DIR exclusion is only needed in copySourceTree (which copies from the project root into staging). By the time we zip, the project config dir was already filtered out — the only agentcore/ in staging is a legitimate dependency package. Closes #843 --- src/lib/packaging/__tests__/helpers.test.ts | 71 ++++++++++++++++++--- src/lib/packaging/helpers.ts | 14 ++-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index a554edb0e..f27c171fa 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -477,8 +477,11 @@ describe('nested agentcore directory is preserved (issue #843)', () => { }); // ── createZipFromDir (async) ── + // The zip stage should NOT exclude agentcore/ — that's copySourceTree's job. + // When zipping a staging directory, any agentcore/ present is a legitimate + // Python package installed by uv, not the project config dir. - it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => { + it('zip: does not exclude agentcore/ directories (staging has no project config)', async () => { const src = buildFixture(join(root, 'zip-async')); const zipPath = join(root, 'zip-async.zip'); @@ -487,21 +490,17 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = await readFile(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - // Top-level agentcore/ should NOT appear - expect(entries.some(e => e === 'agentcore/config.yaml')).toBe(false); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); - - // Nested agentcore/ SHOULD appear + // Both top-level and nested agentcore/ are preserved in the zip — + // the zip function zips everything; exclusion is copySourceTree's concern + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); - - // Regular files present expect(entries).toContain('main.py'); }); // ── createZipFromDirSync ── - it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => { + it('sync zip: does not exclude agentcore/ directories (staging has no project config)', () => { const src = buildFixture(join(root, 'zip-sync')); const zipPath = join(root, 'zip-sync.zip'); @@ -510,9 +509,61 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = readFileSync(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); expect(entries).toContain('main.py'); }); + + // ── Staging directory scenario (the actual bug) ── + // After uv installs deps into staging, copySourceTree copies user source on top. + // The staging dir may contain a top-level agentcore/ from a Python package. + // createZipFromDir must NOT strip it. + + it('zip preserves top-level agentcore/ Python package in staging dir', async () => { + const staging = join(root, 'staging-zip-async'); + mkdirSync(staging, { recursive: true }); + + // Simulate uv-installed dependency with top-level agentcore/ package + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + + // User source copied on top by copySourceTree + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-async.zip'); + await createZipFromDir(staging, zipPath); + + const zipBuffer = await readFile(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); + + it('sync zip preserves top-level agentcore/ Python package in staging dir', () => { + const staging = join(root, 'staging-zip-sync'); + mkdirSync(staging, { recursive: true }); + + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-sync.zip'); + createZipFromDirSync(staging, zipPath); + + const zipBuffer = readFileSync(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); }); diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index 31c74b298..36074395c 100644 --- a/src/lib/packaging/helpers.ts +++ b/src/lib/packaging/helpers.ts @@ -192,24 +192,23 @@ export async function createZipFromDir(sourceDir: string, outputZip: string): Pr await rm(outputZip, { force: true }); await mkdir(dirname(outputZip), { recursive: true }); - const files = await collectFiles(sourceDir, sourceDir); + const files = await collectFiles(sourceDir); const zipped = zipSync(files); await writeFile(outputZip, zipped); } -async function collectFiles(directory: string, rootDir: string, basePath = ''): Promise { +async function collectFiles(directory: string, basePath = ''): Promise { const result: Zippable = {}; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, await collectFiles(fullPath, rootDir, zipPath)); + Object.assign(result, await collectFiles(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [await readFile(fullPath), { level: 6 }]; } @@ -325,19 +324,18 @@ export function ensureBinaryAvailableSync(binary: string, installHint?: string): throw new MissingDependencyError(binary, installHint); } -function collectFilesSync(directory: string, rootDir: string, basePath = ''): Zippable { +function collectFilesSync(directory: string, basePath = ''): Zippable { const result: Zippable = {}; const entries = readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath)); + Object.assign(result, collectFilesSync(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [readFileSync(fullPath), { level: 6 }]; } @@ -349,7 +347,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void rmSync(outputZip, { force: true }); mkdirSync(dirname(outputZip), { recursive: true }); - const files = collectFilesSync(sourceDir, sourceDir); + const files = collectFilesSync(sourceDir); const zipped = zipSync(files); writeFileSync(outputZip, zipped); }