Skip to content
Merged
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
128 changes: 128 additions & 0 deletions src/lib/packaging/__tests__/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
resolveProjectPaths,
resolveProjectPathsSync,
} from '../helpers.js';
import { unzipSync } from 'fflate';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { readFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
Expand Down Expand Up @@ -388,3 +390,129 @@ describe('convertWindowsScriptsToLinux (shebang rewriting on non-Windows)', () =
expect(existsSync(join(staging, 'bin'))).toBe(false);
});
});

// ── Issue #843: nested agentcore directory exclusion ────────────────

describe('nested agentcore directory is preserved (issue #843)', () => {
let root: string;

beforeAll(() => {
root = mkdtempSync(join(tmpdir(), 'helpers-nested-agentcore-'));
});

afterAll(() => {
rmSync(root, { recursive: true, force: true });
});

/**
* Helper: build a source tree that mimics a real project with a
* top-level agentcore/ config dir AND a third-party dependency
* that ships its own agentcore/ sub-module.
*
* <src>/
* main.py
* agentcore/ ← should be excluded (project config dir)
* config.yaml
* lib/
* langgraph_checkpoint_aws/
* __init__.py
* agentcore/ ← should be INCLUDED (dependency sub-module)
* __init__.py
* core.py
*/
function buildFixture(base: string): string {
const src = join(base, 'src');

// Top-level source file
mkdirSync(src, { recursive: true });
writeFileSync(join(src, 'main.py'), 'print("hello")');

// Top-level agentcore/ (project config — should be excluded)
mkdirSync(join(src, 'agentcore'), { recursive: true });
writeFileSync(join(src, 'agentcore', 'config.yaml'), 'key: value');

// Nested dependency with its own agentcore/ sub-module
const nestedAgentcore = join(src, 'lib', 'langgraph_checkpoint_aws', 'agentcore');
mkdirSync(nestedAgentcore, { recursive: true });
writeFileSync(join(src, 'lib', 'langgraph_checkpoint_aws', '__init__.py'), '# init');
writeFileSync(join(nestedAgentcore, '__init__.py'), '# agentcore init');
writeFileSync(join(nestedAgentcore, 'core.py'), 'class Core: pass');

return src;
}

// ── copySourceTree (async) ──

it('excludes top-level agentcore/ but includes nested agentcore/', async () => {
const src = buildFixture(join(root, 'copy-async'));
const dest = join(root, 'copy-async-dest');
mkdirSync(dest, { recursive: true });

await copySourceTree(src, dest);

// Top-level agentcore/ excluded
expect(existsSync(join(dest, 'agentcore'))).toBe(false);

// Source file preserved
expect(existsSync(join(dest, 'main.py'))).toBe(true);

// Nested agentcore/ inside dependency preserved
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);
});

// ── copySourceTreeSync ──

it('sync: excludes top-level agentcore/ but includes nested agentcore/', () => {
const src = buildFixture(join(root, 'copy-sync'));
const dest = join(root, 'copy-sync-dest');
mkdirSync(dest, { recursive: true });

copySourceTreeSync(src, dest);

expect(existsSync(join(dest, 'agentcore'))).toBe(false);
expect(existsSync(join(dest, 'main.py'))).toBe(true);
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);
});

// ── createZipFromDir (async) ──

it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => {
const src = buildFixture(join(root, 'zip-async'));
const zipPath = join(root, 'zip-async.zip');

await createZipFromDir(src, 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('main.py');
});

// ── createZipFromDirSync ──

it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => {
const src = buildFixture(join(root, 'zip-sync'));
const zipPath = join(root, 'zip-sync.zip');

createZipFromDirSync(src, 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('main.py');
});
});
42 changes: 21 additions & 21 deletions src/lib/packaging/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,7 @@ interface ResolvedPaths {
artifactsDir: string;
}

const EXCLUDED_ENTRIES = new Set([
'agentcore',
'.git',
'.venv',
'__pycache__',
'.pytest_cache',
'.DS_Store',
'node_modules',
]);
const EXCLUDED_ENTRIES = new Set(['.git', '.venv', '__pycache__', '.pytest_cache', '.DS_Store', 'node_modules']);

export const MAX_ZIP_SIZE_BYTES = 250 * 1024 * 1024;

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

async function copyEntry(source: string, destination: string): Promise<void> {
async function copyEntry(source: string, destination: string, rootDir: string): Promise<void> {
const stats = await stat(source);
if (stats.isDirectory()) {
await mkdir(destination, { recursive: true });
Expand All @@ -156,7 +148,10 @@ async function copyEntry(source: string, destination: string): Promise<void> {
if (EXCLUDED_ENTRIES.has(entry)) {
continue;
}
await copyEntry(join(source, entry), join(destination, entry));
if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) {
continue;
}
await copyEntry(join(source, entry), join(destination, entry), rootDir);
}
return;
}
Expand All @@ -170,7 +165,7 @@ export async function copySourceTree(srcDir: string, destination: string): Promi
if (!(await pathExists(srcDir))) {
throw new MissingProjectFileError(srcDir);
}
await copyEntry(srcDir, destination);
await copyEntry(srcDir, destination, srcDir);
}

export async function ensureBinaryAvailable(binary: string, installHint?: string): Promise<void> {
Expand All @@ -197,23 +192,24 @@ 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);
const files = await collectFiles(sourceDir, sourceDir);
const zipped = zipSync(files);
await writeFile(outputZip, zipped);
}

async function collectFiles(directory: string, basePath = ''): Promise<Zippable> {
async function collectFiles(directory: string, rootDir: 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, zipPath));
Object.assign(result, await collectFiles(fullPath, rootDir, zipPath));
} else if (entry.isFile()) {
result[zipPath] = [await readFile(fullPath), { level: 6 }];
}
Expand Down Expand Up @@ -286,7 +282,7 @@ export function ensureDirCleanSync(dir: string): void {
mkdirSync(dir, { recursive: true });
}

function copyEntrySync(source: string, destination: string): void {
function copyEntrySync(source: string, destination: string, rootDir: string): void {
const stats = statSync(source);
if (stats.isDirectory()) {
mkdirSync(destination, { recursive: true });
Expand All @@ -295,7 +291,10 @@ function copyEntrySync(source: string, destination: string): void {
if (EXCLUDED_ENTRIES.has(entry)) {
continue;
}
copyEntrySync(join(source, entry), join(destination, entry));
if (entry === CONFIG_DIR && resolve(source) === resolve(rootDir)) {
continue;
}
copyEntrySync(join(source, entry), join(destination, entry), rootDir);
}
return;
}
Expand All @@ -307,7 +306,7 @@ export function copySourceTreeSync(srcDir: string, destination: string): void {
if (!pathExistsSync(srcDir)) {
throw new MissingProjectFileError(srcDir);
}
copyEntrySync(srcDir, destination);
copyEntrySync(srcDir, destination, srcDir);
}

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

function collectFilesSync(directory: string, basePath = ''): Zippable {
function collectFilesSync(directory: string, rootDir: 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, zipPath));
Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath));
} else if (entry.isFile()) {
result[zipPath] = [readFileSync(fullPath), { level: 6 }];
}
Expand All @@ -349,7 +349,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void
rmSync(outputZip, { force: true });
mkdirSync(dirname(outputZip), { recursive: true });

const files = collectFilesSync(sourceDir);
const files = collectFilesSync(sourceDir, sourceDir);
const zipped = zipSync(files);
writeFileSync(outputZip, zipped);
}
Expand Down
13 changes: 0 additions & 13 deletions src/lib/packaging/types/fflate.d.ts

This file was deleted.

Loading