Skip to content

Commit c3921ec

Browse files
authored
fix: only exclude root-level agentcore/ directory from packaging artifacts (#844)
The EXCLUDED_ENTRIES set unconditionally excluded any directory named 'agentcore' at any depth during zip/copy operations. This silently dropped third-party dependency sub-modules that happen to use the same directory name (e.g., langgraph_checkpoint_aws/agentcore/), causing ImportError at runtime. Remove 'agentcore' from the flat EXCLUDED_ENTRIES set and instead thread the original rootDir through all recursive traversal functions. The agentcore directory is now only excluded when its resolved path matches join(rootDir, CONFIG_DIR) — i.e., it sits at the project root. Also remove the hand-written fflate type shim (src/lib/packaging/types/fflate.d.ts) that shadowed the package's own type declarations. The shim only declared zipSync, making all other fflate exports (including unzipSync) invisible to TypeScript. The real fflate v0.8.2 ships complete types that resolve correctly under moduleResolution: "bundler". Closes #843
1 parent 5962711 commit c3921ec

3 files changed

Lines changed: 149 additions & 34 deletions

File tree

src/lib/packaging/__tests__/helpers.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
resolveProjectPaths,
1717
resolveProjectPathsSync,
1818
} from '../helpers.js';
19+
import { unzipSync } from 'fflate';
1920
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
21+
import { readFile } from 'fs/promises';
2022
import { tmpdir } from 'os';
2123
import { join } from 'path';
2224
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -388,3 +390,129 @@ describe('convertWindowsScriptsToLinux (shebang rewriting on non-Windows)', () =
388390
expect(existsSync(join(staging, 'bin'))).toBe(false);
389391
});
390392
});
393+
394+
// ── Issue #843: nested agentcore directory exclusion ────────────────
395+
396+
describe('nested agentcore directory is preserved (issue #843)', () => {
397+
let root: string;
398+
399+
beforeAll(() => {
400+
root = mkdtempSync(join(tmpdir(), 'helpers-nested-agentcore-'));
401+
});
402+
403+
afterAll(() => {
404+
rmSync(root, { recursive: true, force: true });
405+
});
406+
407+
/**
408+
* Helper: build a source tree that mimics a real project with a
409+
* top-level agentcore/ config dir AND a third-party dependency
410+
* that ships its own agentcore/ sub-module.
411+
*
412+
* <src>/
413+
* main.py
414+
* agentcore/ ← should be excluded (project config dir)
415+
* config.yaml
416+
* lib/
417+
* langgraph_checkpoint_aws/
418+
* __init__.py
419+
* agentcore/ ← should be INCLUDED (dependency sub-module)
420+
* __init__.py
421+
* core.py
422+
*/
423+
function buildFixture(base: string): string {
424+
const src = join(base, 'src');
425+
426+
// Top-level source file
427+
mkdirSync(src, { recursive: true });
428+
writeFileSync(join(src, 'main.py'), 'print("hello")');
429+
430+
// Top-level agentcore/ (project config — should be excluded)
431+
mkdirSync(join(src, 'agentcore'), { recursive: true });
432+
writeFileSync(join(src, 'agentcore', 'config.yaml'), 'key: value');
433+
434+
// Nested dependency with its own agentcore/ sub-module
435+
const nestedAgentcore = join(src, 'lib', 'langgraph_checkpoint_aws', 'agentcore');
436+
mkdirSync(nestedAgentcore, { recursive: true });
437+
writeFileSync(join(src, 'lib', 'langgraph_checkpoint_aws', '__init__.py'), '# init');
438+
writeFileSync(join(nestedAgentcore, '__init__.py'), '# agentcore init');
439+
writeFileSync(join(nestedAgentcore, 'core.py'), 'class Core: pass');
440+
441+
return src;
442+
}
443+
444+
// ── copySourceTree (async) ──
445+
446+
it('excludes top-level agentcore/ but includes nested agentcore/', async () => {
447+
const src = buildFixture(join(root, 'copy-async'));
448+
const dest = join(root, 'copy-async-dest');
449+
mkdirSync(dest, { recursive: true });
450+
451+
await copySourceTree(src, dest);
452+
453+
// Top-level agentcore/ excluded
454+
expect(existsSync(join(dest, 'agentcore'))).toBe(false);
455+
456+
// Source file preserved
457+
expect(existsSync(join(dest, 'main.py'))).toBe(true);
458+
459+
// Nested agentcore/ inside dependency preserved
460+
expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', '__init__.py'))).toBe(true);
461+
expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', 'core.py'))).toBe(true);
462+
});
463+
464+
// ── copySourceTreeSync ──
465+
466+
it('sync: excludes top-level agentcore/ but includes nested agentcore/', () => {
467+
const src = buildFixture(join(root, 'copy-sync'));
468+
const dest = join(root, 'copy-sync-dest');
469+
mkdirSync(dest, { recursive: true });
470+
471+
copySourceTreeSync(src, dest);
472+
473+
expect(existsSync(join(dest, 'agentcore'))).toBe(false);
474+
expect(existsSync(join(dest, 'main.py'))).toBe(true);
475+
expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', '__init__.py'))).toBe(true);
476+
expect(existsSync(join(dest, 'lib', 'langgraph_checkpoint_aws', 'agentcore', 'core.py'))).toBe(true);
477+
});
478+
479+
// ── createZipFromDir (async) ──
480+
481+
it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => {
482+
const src = buildFixture(join(root, 'zip-async'));
483+
const zipPath = join(root, 'zip-async.zip');
484+
485+
await createZipFromDir(src, zipPath);
486+
487+
const zipBuffer = await readFile(zipPath);
488+
const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer)));
489+
490+
// Top-level agentcore/ should NOT appear
491+
expect(entries.some(e => e === 'agentcore/config.yaml')).toBe(false);
492+
expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false);
493+
494+
// Nested agentcore/ SHOULD appear
495+
expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py');
496+
expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py');
497+
498+
// Regular files present
499+
expect(entries).toContain('main.py');
500+
});
501+
502+
// ── createZipFromDirSync ──
503+
504+
it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => {
505+
const src = buildFixture(join(root, 'zip-sync'));
506+
const zipPath = join(root, 'zip-sync.zip');
507+
508+
createZipFromDirSync(src, zipPath);
509+
510+
const zipBuffer = readFileSync(zipPath);
511+
const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer)));
512+
513+
expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false);
514+
expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py');
515+
expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py');
516+
expect(entries).toContain('main.py');
517+
});
518+
});

src/lib/packaging/helpers.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,7 @@ interface ResolvedPaths {
5151
artifactsDir: string;
5252
}
5353

54-
const EXCLUDED_ENTRIES = new Set([
55-
'agentcore',
56-
'.git',
57-
'.venv',
58-
'__pycache__',
59-
'.pytest_cache',
60-
'.DS_Store',
61-
'node_modules',
62-
]);
54+
const EXCLUDED_ENTRIES = new Set(['.git', '.venv', '__pycache__', '.pytest_cache', '.DS_Store', 'node_modules']);
6355

6456
export const MAX_ZIP_SIZE_BYTES = 250 * 1024 * 1024;
6557

@@ -147,7 +139,7 @@ export async function ensureDirClean(dir: string): Promise<void> {
147139
await mkdir(dir, { recursive: true });
148140
}
149141

150-
async function copyEntry(source: string, destination: string): Promise<void> {
142+
async function copyEntry(source: string, destination: string, rootDir: string): Promise<void> {
151143
const stats = await stat(source);
152144
if (stats.isDirectory()) {
153145
await mkdir(destination, { recursive: true });
@@ -156,7 +148,10 @@ async function copyEntry(source: string, destination: string): Promise<void> {
156148
if (EXCLUDED_ENTRIES.has(entry)) {
157149
continue;
158150
}
159-
await copyEntry(join(source, entry), join(destination, entry));
151+
if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) {
152+
continue;
153+
}
154+
await copyEntry(join(source, entry), join(destination, entry), rootDir);
160155
}
161156
return;
162157
}
@@ -170,7 +165,7 @@ export async function copySourceTree(srcDir: string, destination: string): Promi
170165
if (!(await pathExists(srcDir))) {
171166
throw new MissingProjectFileError(srcDir);
172167
}
173-
await copyEntry(srcDir, destination);
168+
await copyEntry(srcDir, destination, srcDir);
174169
}
175170

176171
export async function ensureBinaryAvailable(binary: string, installHint?: string): Promise<void> {
@@ -197,23 +192,24 @@ export async function createZipFromDir(sourceDir: string, outputZip: string): Pr
197192
await rm(outputZip, { force: true });
198193
await mkdir(dirname(outputZip), { recursive: true });
199194

200-
const files = await collectFiles(sourceDir);
195+
const files = await collectFiles(sourceDir, sourceDir);
201196
const zipped = zipSync(files);
202197
await writeFile(outputZip, zipped);
203198
}
204199

205-
async function collectFiles(directory: string, basePath = ''): Promise<Zippable> {
200+
async function collectFiles(directory: string, rootDir: string, basePath = ''): Promise<Zippable> {
206201
const result: Zippable = {};
207202
const entries = await readdir(directory, { withFileTypes: true });
208203

209204
for (const entry of entries) {
210205
if (EXCLUDED_ENTRIES.has(entry.name)) continue;
206+
if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue;
211207

212208
const fullPath = join(directory, entry.name);
213209
const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name;
214210

215211
if (entry.isDirectory()) {
216-
Object.assign(result, await collectFiles(fullPath, zipPath));
212+
Object.assign(result, await collectFiles(fullPath, rootDir, zipPath));
217213
} else if (entry.isFile()) {
218214
result[zipPath] = [await readFile(fullPath), { level: 6 }];
219215
}
@@ -286,7 +282,7 @@ export function ensureDirCleanSync(dir: string): void {
286282
mkdirSync(dir, { recursive: true });
287283
}
288284

289-
function copyEntrySync(source: string, destination: string): void {
285+
function copyEntrySync(source: string, destination: string, rootDir: string): void {
290286
const stats = statSync(source);
291287
if (stats.isDirectory()) {
292288
mkdirSync(destination, { recursive: true });
@@ -295,7 +291,10 @@ function copyEntrySync(source: string, destination: string): void {
295291
if (EXCLUDED_ENTRIES.has(entry)) {
296292
continue;
297293
}
298-
copyEntrySync(join(source, entry), join(destination, entry));
294+
if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) {
295+
continue;
296+
}
297+
copyEntrySync(join(source, entry), join(destination, entry), rootDir);
299298
}
300299
return;
301300
}
@@ -307,7 +306,7 @@ export function copySourceTreeSync(srcDir: string, destination: string): void {
307306
if (!pathExistsSync(srcDir)) {
308307
throw new MissingProjectFileError(srcDir);
309308
}
310-
copyEntrySync(srcDir, destination);
309+
copyEntrySync(srcDir, destination, srcDir);
311310
}
312311

313312
export function ensureBinaryAvailableSync(binary: string, installHint?: string): void {
@@ -326,18 +325,19 @@ export function ensureBinaryAvailableSync(binary: string, installHint?: string):
326325
throw new MissingDependencyError(binary, installHint);
327326
}
328327

329-
function collectFilesSync(directory: string, basePath = ''): Zippable {
328+
function collectFilesSync(directory: string, rootDir: string, basePath = ''): Zippable {
330329
const result: Zippable = {};
331330
const entries = readdirSync(directory, { withFileTypes: true });
332331

333332
for (const entry of entries) {
334333
if (EXCLUDED_ENTRIES.has(entry.name)) continue;
334+
if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue;
335335

336336
const fullPath = join(directory, entry.name);
337337
const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name;
338338

339339
if (entry.isDirectory()) {
340-
Object.assign(result, collectFilesSync(fullPath, zipPath));
340+
Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath));
341341
} else if (entry.isFile()) {
342342
result[zipPath] = [readFileSync(fullPath), { level: 6 }];
343343
}
@@ -349,7 +349,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void
349349
rmSync(outputZip, { force: true });
350350
mkdirSync(dirname(outputZip), { recursive: true });
351351

352-
const files = collectFilesSync(sourceDir);
352+
const files = collectFilesSync(sourceDir, sourceDir);
353353
const zipped = zipSync(files);
354354
writeFileSync(outputZip, zipped);
355355
}

src/lib/packaging/types/fflate.d.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)