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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
7 changes: 5 additions & 2 deletions .github/workflows/e2e-tests.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ jobs:
BASE_SHA=${{ github.event.pull_request.base.sha || 'HEAD~1' }}
CHANGED=$(git diff --name-only "$BASE_SHA"..HEAD -- 'e2e-tests/*.test.ts' \
| grep -v '^e2e-tests/strands-bedrock\.test\.ts$' \
| grep -v '^e2e-tests/harness-bedrock\.test\.ts$' \
| tr '\n' ' ')
echo "extra_tests=$CHANGED" >> "$GITHUB_OUTPUT"
echo "Changed e2e tests: ${CHANGED:-none}"
Expand All@@ -113,5 +114,7 @@ jobs:
OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }}
CDK_TARBALL: ${{ env.CDK_TARBALL }}
# Always run strands-bedrock as baseline, plus any e2e test files changed in the PR
run: npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts ${{ steps.changed.outputs.extra_tests }}
# Always run strands-bedrock and harness-bedrock as baseline, plus any e2e test files changed in the PR
run:
npx vitest run --project e2e e2e-tests/strands-bedrock.test.ts e2e-tests/harness-bedrock.test.ts ${{
steps.changed.outputs.extra_tests }}
3 changes: 3 additions & 0 deletions e2e-tests/harness-bedrock.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'bedrock' });
163 changes: 163 additions & 0 deletions e2e-tests/harness-e2e-helper.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
import {
cleanupStaleCredentialProviders,
installCdkTarball,
runAgentCoreCLI,
teardownE2EProject,
writeAwsTargets,
} from './e2e-helper.js';
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';

const hasAws = hasAwsCredentials();
const baseCanRun = prereqs.npm && prereqs.git && hasAws;

interface HarnessE2EConfig {
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
requiredEnvVar?: string;
skipMemory?: boolean;
}

export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
const canRun = baseCanRun && hasRequiredVar;

const providerLabel =
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';

describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
let testDir: string;
let projectPath: string;
let harnessName: string;

beforeAll(async () => {
if (!canRun) return;

await cleanupStaleCredentialProviders();

testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
await mkdir(testDir, { recursive: true });

const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;

const createArgs = [
'create',
'--name',
harnessName,
'--model-provider',
cfg.modelProvider,
'--json',
'--skip-git',
];

if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
}

if (cfg.skipMemory) {
createArgs.push('--no-harness-memory');
}

const result = await runAgentCoreCLI(createArgs, testDir);

expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
const json = parseJsonOutput(result.stdout) as { projectPath: string };
projectPath = json.projectPath;

await writeAwsTargets(projectPath);
installCdkTarball(projectPath);
}, 300000);

afterAll(async () => {
if (projectPath && hasAws) {
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
}
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
}, 600000);

it.skipIf(!canRun)(
'deploys to AWS successfully',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);

if (result.exitCode !== 0) {
console.log('Deploy stdout:', result.stdout);
console.log('Deploy stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Deploy should report success').toBe(true);
},
1,
30000
);
},
600000
);

it.skipIf(!canRun)(
'invokes the deployed harness',
async () => {
expect(projectPath, 'Project should have been created').toBeTruthy();

await retry(
async () => {
const result = await runAgentCoreCLI(
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
projectPath
);

if (result.exitCode !== 0) {
console.log('Invoke stdout:', result.stdout);
console.log('Invoke stderr:', result.stderr);
}

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

const json = parseJsonOutput(result.stdout) as { success: boolean };
expect(json.success, 'Invoke should report success').toBe(true);
},
3,
15000
);
},
180000
);

it.skipIf(!canRun)(
'status shows the deployed harness',
async () => {
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);

expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);

const json = parseJsonOutput(statusResult.stdout) as {
success: boolean;
resources: {
resourceType: string;
name: string;
deploymentState: string;
identifier?: string;
}[];
};
expect(json.success).toBe(true);

const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
expect(harness!.deploymentState).toBe('deployed');
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
},
120000
);
});
}
3 changes: 3 additions & 0 deletions e2e-tests/harness-gemini.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });
3 changes: 3 additions & 0 deletions e2e-tests/harness-openai.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { createHarnessE2ESuite } from './harness-e2e-helper.js';

createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });
204 changes: 204 additions & 0 deletions integ-tests/add-remove-harness.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
import { createTestProject, exists, readProjectConfig, runCLI } from '../src/test-utils/index.js';
import type { TestProject } from '../src/test-utils/index.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

async function readHarnessSpec(projectPath: string, harnessName: string) {
return JSON.parse(await readFile(join(projectPath, `app/${harnessName}/harness.json`), 'utf-8'));
}

describe('integration: harness add/remove lifecycle', () => {
let project: TestProject;
const harnessName = 'TestHarness';

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds a harness with defaults', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness, `Harness "${harnessName}" should be in agentcore.json`).toBeTruthy();
expect(harness!.path).toBe(`app/${harnessName}`);
});

it('creates harness.json with correct model config', async () => {
const spec = await readHarnessSpec(project.projectPath, harnessName);
expect(spec.model).toBeDefined();
expect(spec.model.provider).toBe('bedrock');
expect(spec.model.modelId).toBeTruthy();
});

it('creates system-prompt.md', async () => {
const promptPath = join(project.projectPath, `app/${harnessName}/system-prompt.md`);
expect(await exists(promptPath), 'system-prompt.md should exist').toBe(true);
});

it('auto-creates memory resource', async () => {
const config = await readProjectConfig(project.projectPath);
const memories = config.memories ?? [];
expect(memories.length, 'Should have auto-created memory').toBeGreaterThan(0);
});

it('rejects duplicate harness name', async () => {
const result = await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('removes the harness', async () => {
const result = await runCLI(['remove', 'harness', '--name', harnessName, '--json'], project.projectPath);

expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.success).toBe(true);

const config = await readProjectConfig(project.projectPath);
const found = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(found, `Harness "${harnessName}" should be removed`).toBeFalsy();
});
});

describe('integration: harness configuration options', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('adds harness with truncation strategy', async () => {
const name = 'TruncHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--truncation-strategy', 'sliding_window', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.truncation?.strategy).toBe('sliding_window');
});

it('adds harness with lifecycle config', async () => {
const name = 'LifecycleHarness';
const result = await runCLI(
['add', 'harness', '--name', name, '--idle-timeout', '300', '--max-lifetime', '3600', '--json'],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.lifecycleConfig?.idleRuntimeSessionTimeout).toBe(300);
expect(spec.lifecycleConfig?.maxLifetime).toBe(3600);
});

it('adds harness without memory when --no-memory is set', async () => {
const name = 'NoMemHarness';
const configBefore = await readProjectConfig(project.projectPath);
const memoriesBefore = (configBefore.memories ?? []).length;

const result = await runCLI(['add', 'harness', '--name', name, '--no-memory', '--json'], project.projectPath);

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

const configAfter = await readProjectConfig(project.projectPath);
const memoriesAfter = (configAfter.memories ?? []).length;
expect(memoriesAfter).toBe(memoriesBefore);
});

it('adds harness with non-bedrock model provider', async () => {
const name = 'OpenAIHarness';
const result = await runCLI(
[
'add',
'harness',
'--name',
name,
'--model-provider',
'open_ai',
'--model-id',
'gpt-5',
'--api-key-arn',
'arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key',
'--json',
],
project.projectPath
);

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

const spec = await readHarnessSpec(project.projectPath, name);
expect(spec.model.provider).toBe('open_ai');
expect(spec.model.modelId).toBe('gpt-5');
expect(spec.model.apiKeyArn).toBe('arn:aws:secretsmanager:us-east-1:123456789012:secret:openai-key');
});
});

describe('integration: harness validation errors', () => {
let project: TestProject;

beforeAll(async () => {
project = await createTestProject({ noAgent: true });
});

afterAll(async () => {
await project.cleanup();
});

it('rejects invalid harness name with special characters', async () => {
const result = await runCLI(['add', 'harness', '--name', 'bad-name!', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects harness name starting with a number', async () => {
const result = await runCLI(['add', 'harness', '--name', '1BadName', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});

it('rejects add harness without --name when --json is passed', async () => {
const result = await runCLI(['add', 'harness', '--json'], project.projectPath);
expect(result.exitCode).not.toBe(0);
});
});

describe('integration: create project with harness', () => {
let project: TestProject;
const harnessName = 'CreateHarness';

beforeAll(async () => {
project = await createTestProject({ name: harnessName, noAgent: true });
await runCLI(['add', 'harness', '--name', harnessName, '--json'], project.projectPath);
});

afterAll(async () => {
await project.cleanup();
});

it('has correct project scaffolding', async () => {
expect(await exists(join(project.projectPath, 'agentcore/agentcore.json'))).toBe(true);
expect(await exists(join(project.projectPath, 'agentcore/cdk'))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/harness.json`))).toBe(true);
expect(await exists(join(project.projectPath, `app/${harnessName}/system-prompt.md`))).toBe(true);
});

it('has harness registered in project config', async () => {
const config = await readProjectConfig(project.projectPath);
const harness = config.harnesses?.find((h: { name: string }) => h.name === harnessName);
expect(harness).toBeTruthy();
});
});
Loading