diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index a554edb0e..4b830ed41 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -475,44 +475,68 @@ describe('nested agentcore directory is preserved (issue #843)', () => { expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', '__init__.py'))).toBe(true); expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', 'core.py'))).toBe(true); }); +}); + +// ── Issue #1408: zip stage must not drop top-level agentcore Python packages ── + +describe('top-level agentcore Python package is preserved in zip (issue #1408)', () => { + let root: string; + + beforeAll(() => { + root = mkdtempSync(join(tmpdir(), 'helpers-zip-agentcore-pkg-')); + }); - // ── createZipFromDir (async) ── + afterAll(() => { + rmSync(root, { recursive: true, force: true }); + }); - it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => { - const src = buildFixture(join(root, 'zip-async')); + /** + * Mimics a staging directory after `uv pip install --target staging` + * has installed a third-party package whose top-level module happens to + * be named `agentcore`. The zip stage runs against staging, where the + * project's own `agentcore/` config dir is already absent, so a + * top-level `agentcore/` here is a real Python package and must be + * included in the deployment artifact. + */ + function buildStagingFixture(base: string): string { + const staging = join(base, 'staging'); + + mkdirSync(staging, { recursive: true }); + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + // Top-level `agentcore` Python package (e.g. an installed dependency) + mkdirSync(join(staging, 'agentcore'), { recursive: true }); + writeFileSync(join(staging, 'agentcore', '__init__.py'), '# package init'); + writeFileSync(join(staging, 'agentcore', 'runtime.py'), 'def run(): pass'); + + return staging; + } + + it('createZipFromDir includes a top-level agentcore Python package', async () => { + const staging = buildStagingFixture(join(root, 'zip-async')); const zipPath = join(root, 'zip-async.zip'); - await createZipFromDir(src, zipPath); + await createZipFromDir(staging, zipPath); 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 - 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('agentcore/__init__.py'); + expect(entries).toContain('agentcore/runtime.py'); expect(entries).toContain('main.py'); }); - // ── createZipFromDirSync ── - - it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => { - const src = buildFixture(join(root, 'zip-sync')); + it('createZipFromDirSync includes a top-level agentcore Python package', () => { + const staging = buildStagingFixture(join(root, 'zip-sync')); const zipPath = join(root, 'zip-sync.zip'); - createZipFromDirSync(src, zipPath); + createZipFromDirSync(staging, zipPath); const zipBuffer = readFileSync(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); - expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); - expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); + expect(entries).toContain('agentcore/__init__.py'); + expect(entries).toContain('agentcore/runtime.py'); expect(entries).toContain('main.py'); }); }); diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index caa3d3792..7a0d72557 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 }]; } @@ -398,19 +397,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 }]; } @@ -422,7 +420,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); }