Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 45 additions & 21 deletions src/lib/packaging/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
14 changes: 6 additions & 8 deletions src/lib/packaging/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Zippable> {
async function collectFiles(directory: string, basePath = ''): Promise<Zippable> {
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 }];
}
Expand Down Expand Up @@ -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 }];
}
Expand All @@ -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);
}
Expand Down