') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat: testing fixes as well as refactoring test organization and to use npm & tsx by aidandaly24 · Pull Request #3 · aws/agentcore-cli · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,6 @@ public github issue.
### Prerequisites

- Node.js 20+
- Bun (for CLI bundling)
- npm

### Building
Expand All@@ -73,6 +72,15 @@ npm install
npm run build
```

### Testing

```bash
npm test # Run all tests
npm run test:watch # Run tests in watch mode
```

See [docs/TESTING.md](docs/TESTING.md) for detailed testing guidelines.

### Local Development with CDK Package

If you're also developing the CDK package (`@aws/agentcore-l3-cdk-constructs`):
Expand Down
116 changes: 116 additions & 0 deletions docs/TESTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
# Testing Guide

## Quick Start

```bash
npm test # Run all tests
npm run test:watch # Run tests in watch mode
```

## Test Organization

### Unit Tests

Unit tests are co-located with source files in `__tests__/` directories:

```
src/cli/commands/add/
├── action.ts
├── command.ts
└── __tests__/
└── add.test.ts
```

### Integration Tests

Integration tests live in `integ-tests/`:

```
integ-tests/
├── create-no-agent.test.ts
├── create-with-agent.test.ts
├── deploy.test.ts
└── ...
```

See [integ-tests/README.md](../integ-tests/README.md) for integration test details.

## Writing Tests

### Imports

Use vitest for all test utilities:

```typescript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
```

### Assertions

Use `expect` assertions:

```typescript
// Equality
expect(result).toBe('expected');
expect(obj).toEqual({ key: 'value' });

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();

// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('message');
```

### Mocking

Use `vi` for mocks:

```typescript
// Mock functions
const mockFn = vi.fn();
mockFn.mockReturnValue('value');
mockFn.mockResolvedValue('async value');

// Spies
vi.spyOn(module, 'method');

// Module mocks
vi.mock('./module');
```

## Test Utilities

### CLI Runner

`src/test-utils/cli-runner.ts` runs CLI commands in tests:

```typescript
import { runCLI } from '../src/test-utils/cli-runner';

const result = await runCLI(['create', '--name', 'test'], tempDir);
expect(result.exitCode).toBe(0);
```

## Configuration

Test configuration is in `vitest.config.ts`:

- Test timeout: 15 seconds
- Hook timeout: 60 seconds
- Test patterns: `src/**/*.test.ts`, `integ-tests/**/*.test.ts`

## Integration Tests

Integration tests require:

- AWS credentials configured
- IAM permissions for CloudFormation operations
- Dedicated test AWS account (recommended)

Run integration tests:

```bash
npm run test:integ
```
27 changes: 17 additions & 10 deletions bun.build.ts → esbuild.config.mjs
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import { createRequire } from 'module';

// Stub plugin for optional dev dependencies
const optionalDepsPlugin = {
name: 'optional-deps',
setup(build: Parameters<Parameters<typeof Bun.build>[0]['plugins'][number]['setup']>[0]) {
setup(build) {
// Stub react-devtools-core (only used when DEV=true)
build.onResolve({ filter: /^react-devtools-core$/ }, () => ({
path: 'react-devtools-core',
Expand All@@ -19,17 +21,18 @@ const optionalDepsPlugin = {
// Text loader plugin for embedding files
const textLoaderPlugin = {
name: 'text-loader',
setup(build: Parameters<Parameters<typeof Bun.build>[0]['plugins'][number]['setup']>[0]) {
setup(build) {
// Handle .md and .txt files as text
build.onLoad({ filter: /\.(md|txt)$/ }, async args => {
const text = await Bun.file(args.path).text();
const text = await fs.promises.readFile(args.path, 'utf8');
return {
contents: `export default ${JSON.stringify(text)};`,
loader: 'js',
};
});
// Handle .ts files in llm-compacted as text (use platform-agnostic pattern)
// Handle .ts files in llm-compacted as text
build.onLoad({ filter: /llm-compacted[/\\].*\.ts$/ }, async args => {
const text = await Bun.file(args.path).text();
const text = await fs.promises.readFile(args.path, 'utf8');
return {
contents: `export default ${JSON.stringify(text)};`,
loader: 'js',
Expand All@@ -38,13 +41,17 @@ const textLoaderPlugin = {
},
};

await Bun.build({
entrypoints: ['./src/cli/index.ts'],
outdir: './dist/cli',
target: 'node',
await esbuild.build({
entryPoints: ['./src/cli/index.ts'],
outfile: './dist/cli/index.mjs',
bundle: true,
platform: 'node',
format: 'esm',
minify: true,
naming: '[dir]/[name].mjs',
// Inject require shim for ESM compatibility with CommonJS dependencies
banner: {
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
},
external: ['fsevents', '@aws-cdk/toolkit-lib'],
plugins: [optionalDepsPlugin, textLoaderPlugin],
});
Expand Down
8 changes: 4 additions & 4 deletions integ-tests/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,7 +15,7 @@ This directory contains real AWS integration tests that actually deploy resource
npm run test:integ

# Run a specific test
bun test --timeout 300000 integ-tests/integ.deploy.ts
npx vitest run integ-tests/deploy.test.ts --testTimeout=300000
```

## Test Naming Convention
Expand All@@ -38,14 +38,14 @@ Integration tests are NOT run automatically on every PR. They can be triggered:
## Writing Integration Tests

```typescript
import { runCLI } from '../src/test-utils';
import { after, before, describe, it } from 'node:test';
import { runCLI } from '../src/test-utils/cli-runner';
import { afterAll, describe, expect, it } from 'vitest';

describe('integ: deploy', () => {
// Use unique stack names to avoid conflicts
const stackName = `test-${Date.now()}`;

after(async () => {
afterAll(async () => {
// ALWAYS clean up - destroy the stack
await runCLI(['destroy', '--target', stackName, '--force'], projectDir);
});
Expand Down
15 changes: 7 additions & 8 deletions integ-tests/create-no-agent.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
import { exists, runCLI } from '../src/test-utils/index.js';
import { afterAll, beforeAll, describe, it } from 'bun:test';
import assert from 'node:assert';
import { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

function hasCommand(cmd: string): boolean {
try {
Expand DownExpand Up@@ -35,22 +34,22 @@ describe('integration: create without agent', () => {
const name = `IntegNoAgent${Date.now()}`;
const result = await runCLI(['create', '--name', name, '--no-agent', '--json'], testDir, false);

assert.strictEqual(result.exitCode, 0, `stderr: ${result.stderr}`);
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
assert.strictEqual(json.success, true);
expect(json.success).toBe(true);

// Verify npm install ran (in CDK project directory)
assert.ok(
expect(
await exists(join(json.projectPath, 'agentcore', 'cdk', 'node_modules')),
'agentcore/cdk/node_modules/ should exist'
);
).toBeTruthy();

// Verify git init ran
assert.ok(await exists(join(json.projectPath, '.git')), '.git/ should exist');
expect(await exists(join(json.projectPath, '.git')), '.git/ should exist').toBeTruthy();

// Verify at least one commit
const gitLog = execSync('git log --oneline', { cwd: json.projectPath, encoding: 'utf-8' });
assert.ok(gitLog.trim().length > 0, 'Should have at least one commit');
expect(gitLog.trim().length > 0, 'Should have at least one commit').toBeTruthy();
});
});
16 changes: 9 additions & 7 deletions integ-tests/create-with-agent.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
import { exists, runCLI } from '../src/test-utils/index.js';
import { afterAll, beforeAll, describe, it } from 'bun:test';
import assert from 'node:assert';
import { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

function hasCommand(cmd: string): boolean {
try {
Expand DownExpand Up@@ -53,19 +52,22 @@ describe('integration: create with Python agent', () => {
false
);

assert.strictEqual(result.exitCode, 0, `stderr: ${result.stderr}`);
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
assert.strictEqual(json.success, true);
expect(json.success).toBe(true);

// Verify npm install ran
assert.ok(await exists(join(json.projectPath, 'agentcore', 'cdk', 'node_modules')), 'node_modules/ should exist');
expect(
await exists(join(json.projectPath, 'agentcore', 'cdk', 'node_modules')),
'node_modules/ should exist'
).toBeTruthy();

// Verify git init ran
assert.ok(await exists(join(json.projectPath, '.git')), '.git/ should exist');
expect(await exists(join(json.projectPath, '.git')), '.git/ should exist').toBeTruthy();

// Verify uv venv ran - .venv in app/{agentName} directory
const agentDir = join(json.projectPath, 'app', json.agentName || name);
assert.ok(await exists(join(agentDir, '.venv')), '.venv/ should exist in agent directory');
expect(await exists(join(agentDir, '.venv')), '.venv/ should exist in agent directory').toBeTruthy();
});
});
13 changes: 6 additions & 7 deletions integ-tests/deploy.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
import { runCLI } from '../src/test-utils/index.js';
import { afterAll, beforeAll, describe, it } from 'bun:test';
import assert from 'node:assert';
import { execSync } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

function hasCommand(cmd: string): boolean {
try {
Expand DownExpand Up@@ -89,17 +88,17 @@ describe('integration: deploy', () => {
const result = await runCLI(['destroy', '--target', targetName, '--yes', '--json'], projectPath, false);

// Assert destroy succeeded
assert.strictEqual(result.exitCode, 0, `Destroy failed: ${result.stderr}`);
expect(result.exitCode, `Destroy failed: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
assert.strictEqual(json.success, true, 'Destroy should report success');
expect(json.success, 'Destroy should report success').toBe(true);
}
await rm(testDir, { recursive: true, force: true });
}, 120000);

it.skipIf(!hasNpm || !hasGit || !hasUv || !hasAws)(
'deploys to AWS successfully',
async () => {
assert.ok(projectPath, 'Project should have been created');
expect(projectPath, 'Project should have been created').toBeTruthy();

const result = await runCLI(['deploy', '--target', targetName, '--yes', '--json'], projectPath, false);

Expand All@@ -108,10 +107,10 @@ describe('integration: deploy', () => {
console.log('Deploy stderr:', result.stderr);
}

assert.strictEqual(result.exitCode, 0, `Deploy failed: ${result.stderr}`);
expect(result.exitCode, `Deploy failed: ${result.stderr}`).toBe(0);

const json = JSON.parse(result.stdout);
assert.strictEqual(json.success, true, 'Deploy should report success');
expect(json.success, 'Deploy should report success').toBe(true);
},
180000
);
Expand Down
Loading