mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
4537 lines
144 KiB
TypeScript
4537 lines
144 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
|
|
import * as ts from "typescript";
|
|
import { expect, test } from "vitest";
|
|
|
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
const integrationsDir = path.join(repoRoot, "examples", "integrations");
|
|
|
|
const MANAGED_API_KEY = "CPK_INTELLIGENCE_API_KEY";
|
|
const MANAGED_API_KEY_SENTINEL = "cpk_secret_must_not_reach_browser_assets";
|
|
const OPTIONAL_TELEMETRY_ID = "CPK_TELEMETRY_ID";
|
|
const LEGACY_API_KEY = ["INTELLIGENCE", "API", "KEY"].join("_");
|
|
const LEGACY_TELEMETRY_ID = "COPILOTKIT_TELEMETRY_ID";
|
|
const MANAGED_LICENSE_TOKEN = "COPILOTKIT_LICENSE_TOKEN";
|
|
const MANAGED_API_KEY_SECRET_CONFIG =
|
|
"copilotkit_intelligence_api_key_secret_name";
|
|
const MANAGED_API_KEY_SECRET_VERSION_ID =
|
|
"CPK_INTELLIGENCE_API_KEY_SECRET_VERSION_ID";
|
|
const MANAGED_API_KEY_SECRET_VERSION_SENTINEL =
|
|
"agentcore-secret-version-contract";
|
|
const INTELLIGENCE_API_URL = "INTELLIGENCE_API_URL";
|
|
const INTELLIGENCE_GATEWAY_WS_URL = "INTELLIGENCE_GATEWAY_WS_URL";
|
|
const NEXT_THREADS_GATE = "NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED";
|
|
const VITE_THREADS_GATE = "VITE_COPILOTKIT_THREADS_ENABLED";
|
|
const MANAGED_DOCUMENTATION_LABEL = /\bmanaged\b/i;
|
|
const SELF_HOSTED_OR_OFFLINE_LABEL = /\b(?:self[ -]?hosted|offline)\b/i;
|
|
const LICENSE_GATING_LANGUAGE =
|
|
/(?:\blicen[cs]e\b[\s\S]{0,160}\b(?:activat(?:e[sd]?|ing)|unlock(?:s|ed|ing)?|enabl(?:e[sd]?|ing))\b[\s\S]{0,160}\b(?:threads?|inspector)\b|\b(?:threads?|inspector)\b[\s\S]{0,160}\b(?:activat(?:e[sd]?|ing)|unlock(?:s|ed|ing)?|enabl(?:e[sd]?|ing))\b[\s\S]{0,160}\blicen[cs]e\b)/i;
|
|
const INTEGRATION_PARITY_WORKFLOW = path.join(
|
|
repoRoot,
|
|
".github",
|
|
"workflows",
|
|
"integrations_parity.yml",
|
|
);
|
|
|
|
type ManagedSdkPackageName = "@copilotkit/runtime" | "@copilotkit/react-core";
|
|
|
|
interface ManagedSdkPin {
|
|
readonly packageName: ManagedSdkPackageName;
|
|
readonly packagePath: string;
|
|
readonly version: string;
|
|
}
|
|
|
|
const MANAGED_ENTITLEMENT_CONTRACT_BY_SDK_VERSION = {
|
|
"@copilotkit/runtime": {
|
|
"1.62.2": false,
|
|
"1.62.3": false,
|
|
"1.68.1": false,
|
|
"1.70.0": true,
|
|
"1.70.2": true,
|
|
},
|
|
"@copilotkit/react-core": {
|
|
"1.62.2": false,
|
|
"1.62.3": false,
|
|
"1.68.1": false,
|
|
"1.70.0": true,
|
|
"1.70.2": true,
|
|
},
|
|
} as const satisfies Readonly<
|
|
Record<ManagedSdkPackageName, Readonly<Record<string, boolean>>>
|
|
>;
|
|
|
|
const INTELLIGENCE_CLI_FRAMEWORKS = [
|
|
"langgraph-py",
|
|
"langgraph-js",
|
|
"claude-sdk-typescript",
|
|
"claude-sdk-python",
|
|
"flows",
|
|
"mastra",
|
|
"pydantic-ai",
|
|
"llamaindex",
|
|
"agno",
|
|
"adk",
|
|
"aws-strands-py",
|
|
"a2a",
|
|
"microsoft-agent-framework-dotnet",
|
|
"microsoft-agent-framework-py",
|
|
"mcp-apps",
|
|
"agentcore-langgraph",
|
|
"agentcore-strands",
|
|
"a2ui",
|
|
"opengenui",
|
|
] as const;
|
|
|
|
const INTELLIGENCE_DEV_STACKS = [
|
|
{
|
|
integration: "adk",
|
|
projectName: "copilotkit-intelligence-dev-adk",
|
|
postgresPort: "5482",
|
|
redisPort: "6389",
|
|
appApiPort: "4201",
|
|
gatewayPort: "4401",
|
|
},
|
|
{
|
|
integration: "agno",
|
|
projectName: "copilotkit-intelligence-dev-agno",
|
|
postgresPort: "5483",
|
|
redisPort: "6390",
|
|
appApiPort: "4202",
|
|
gatewayPort: "4402",
|
|
},
|
|
{
|
|
integration: "llamaindex",
|
|
projectName: "copilotkit-intelligence-dev-llamaindex",
|
|
postgresPort: "5484",
|
|
redisPort: "6391",
|
|
appApiPort: "4203",
|
|
gatewayPort: "4403",
|
|
},
|
|
{
|
|
integration: "mastra",
|
|
projectName: "copilotkit-intelligence-dev-mastra",
|
|
postgresPort: "5485",
|
|
redisPort: "6392",
|
|
appApiPort: "4204",
|
|
gatewayPort: "4404",
|
|
},
|
|
{
|
|
integration: "ms-agent-framework-python",
|
|
projectName: "copilotkit-intelligence-dev-ms-agent-framework-python",
|
|
postgresPort: "5486",
|
|
redisPort: "6393",
|
|
appApiPort: "4205",
|
|
gatewayPort: "4405",
|
|
},
|
|
{
|
|
integration: "pydantic-ai",
|
|
projectName: "copilotkit-intelligence-dev-pydantic-ai",
|
|
postgresPort: "5487",
|
|
redisPort: "6394",
|
|
appApiPort: "4206",
|
|
gatewayPort: "4406",
|
|
},
|
|
{
|
|
integration: "strands-python",
|
|
projectName: "copilotkit-intelligence-dev-strands-python",
|
|
postgresPort: "5488",
|
|
redisPort: "6395",
|
|
appApiPort: "4207",
|
|
gatewayPort: "4407",
|
|
},
|
|
] as const;
|
|
|
|
const EXPECTED_POSTGRES_INIT_SQL = [
|
|
"-- Runs once on the postgres container's first boot (docker-entrypoint-initdb.d).",
|
|
"-- The intelligence composite image's migrations oneshot + app-api connect to",
|
|
"-- intelligence_app; graphile-migrate uses intelligence_app_shadow for its shadow",
|
|
"-- database.",
|
|
"CREATE DATABASE intelligence_app;",
|
|
"CREATE DATABASE intelligence_app_shadow;",
|
|
"",
|
|
].join("\n");
|
|
|
|
type ManagedCliFramework = (typeof INTELLIGENCE_CLI_FRAMEWORKS)[number];
|
|
|
|
interface AgentOptionContract {
|
|
readonly property: string;
|
|
readonly environmentReads?: readonly string[];
|
|
readonly stringLiterals?: readonly string[];
|
|
}
|
|
|
|
type RuntimeAgentContract =
|
|
| {
|
|
readonly registration: "default";
|
|
readonly constructorName: string;
|
|
readonly options: readonly AgentOptionContract[];
|
|
}
|
|
| {
|
|
readonly registration: "factory";
|
|
readonly calleePath: readonly string[];
|
|
readonly argumentIdentifier: string;
|
|
};
|
|
|
|
interface ManagedTemplateContract {
|
|
readonly directory: string;
|
|
readonly frameworks: readonly ManagedCliFramework[];
|
|
readonly runtimePath: string;
|
|
readonly gatePath: string;
|
|
readonly envPath: string;
|
|
readonly readmePath: string;
|
|
readonly runtimeAgent?: RuntimeAgentContract;
|
|
readonly supportedPaths?: {
|
|
readonly localComposePath: string;
|
|
readonly deploymentConfigPath: string;
|
|
readonly runtimeDeploymentPath: string;
|
|
readonly frontendDeploymentPath: string;
|
|
readonly terraformRuntimeDeploymentPath: string;
|
|
readonly terraformReadmePath: string;
|
|
readonly deployScriptPaths: readonly string[];
|
|
};
|
|
}
|
|
|
|
type AgentCoreDeployScriptName = "deploy-langgraph.sh" | "deploy-strands.sh";
|
|
|
|
interface AgentCoreDeployEnvironment {
|
|
readonly apiUrl?: string;
|
|
readonly gatewayWsUrl?: string;
|
|
}
|
|
|
|
interface AgentCoreDeployHarnessOptions {
|
|
readonly scriptName: AgentCoreDeployScriptName;
|
|
readonly envFile: AgentCoreDeployEnvironment;
|
|
readonly callerEnvironment?: AgentCoreDeployEnvironment;
|
|
readonly skipBackend?: boolean;
|
|
readonly skipFrontend?: boolean;
|
|
readonly xtrace?: boolean;
|
|
}
|
|
|
|
interface AgentCoreDeployHarnessResult {
|
|
readonly status: number | null;
|
|
readonly output: string;
|
|
readonly cdkEnvironment: string | null;
|
|
readonly secretVersionId: string | null;
|
|
readonly awsCommands: readonly string[];
|
|
readonly npmCommands: readonly string[];
|
|
readonly frontendStackName: string | null;
|
|
readonly frontendSecretVersionId: string | null;
|
|
readonly secretExposureObservations: readonly string[];
|
|
readonly secretInputObservations: readonly string[];
|
|
}
|
|
|
|
const LANGGRAPH_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "LangGraphAgent",
|
|
options: [
|
|
{
|
|
property: "deploymentUrl",
|
|
environmentReads: ["AGENT_URL", "LANGGRAPH_DEPLOYMENT_URL"],
|
|
stringLiterals: ["http://localhost:8123"],
|
|
},
|
|
{ property: "graphId", stringLiterals: ["sample_agent"] },
|
|
{
|
|
property: "langsmithApiKey",
|
|
environmentReads: ["LANGSMITH_API_KEY"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const LANGGRAPH_FASTAPI_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "LangGraphHttpAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL"],
|
|
stringLiterals: ["http://localhost:8123"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const HTTP_LOCALHOST_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "HttpAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL"],
|
|
stringLiterals: ["http://localhost:8000"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "HttpAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL"],
|
|
stringLiterals: ["http://localhost:8000/"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const MASTRA_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "factory",
|
|
calleePath: ["MastraAgent", "getLocalAgents"],
|
|
argumentIdentifier: "mastra",
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const LLAMAINDEX_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "LlamaIndexAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL"],
|
|
stringLiterals: ["http://127.0.0.1:9000", "/run"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const AGNO_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "HttpAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL"],
|
|
stringLiterals: ["http://localhost:8000", "/agui"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const STRANDS_RUNTIME_AGENT_CONTRACT = {
|
|
registration: "default",
|
|
constructorName: "HttpAgent",
|
|
options: [
|
|
{
|
|
property: "url",
|
|
environmentReads: ["AGENT_URL", "STRANDS_AGENT_URL"],
|
|
stringLiterals: ["http://localhost:8000"],
|
|
},
|
|
],
|
|
} as const satisfies RuntimeAgentContract;
|
|
|
|
const INTELLIGENCE_TEMPLATE_CONTRACTS = [
|
|
{
|
|
directory: "langgraph-python",
|
|
frameworks: ["langgraph-py", "a2ui", "opengenui"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: LANGGRAPH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "langgraph-js",
|
|
frameworks: ["langgraph-js"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: LANGGRAPH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "langgraph-fastapi",
|
|
frameworks: [],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: LANGGRAPH_FASTAPI_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "claude-sdk-typescript",
|
|
frameworks: ["claude-sdk-typescript"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "claude-sdk-python",
|
|
frameworks: ["claude-sdk-python"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "crewai-flows",
|
|
frameworks: ["flows"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "mastra",
|
|
frameworks: ["mastra"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: MASTRA_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "pydantic-ai",
|
|
frameworks: ["pydantic-ai"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "llamaindex",
|
|
frameworks: ["llamaindex"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: LLAMAINDEX_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "agno",
|
|
frameworks: ["agno"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: AGNO_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "adk",
|
|
frameworks: ["adk"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "strands-python",
|
|
frameworks: ["aws-strands-py"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: STRANDS_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "a2a-middleware",
|
|
frameworks: ["a2a"],
|
|
runtimePath: "app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
},
|
|
{
|
|
directory: "ms-agent-framework-dotnet",
|
|
frameworks: ["microsoft-agent-framework-dotnet"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "ms-agent-framework-python",
|
|
frameworks: ["microsoft-agent-framework-py"],
|
|
runtimePath: "src/app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
runtimeAgent: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
directory: "mcp-apps",
|
|
frameworks: ["mcp-apps"],
|
|
runtimePath: "app/api/copilotkit/[[...slug]]/route.ts",
|
|
gatePath: "next.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
},
|
|
{
|
|
directory: "agentcore",
|
|
frameworks: ["agentcore-langgraph", "agentcore-strands"],
|
|
runtimePath: "infra-cdk/lambdas/copilotkit-runtime/src/runtime.ts",
|
|
gatePath: "frontend/vite.config.ts",
|
|
envPath: ".env.example",
|
|
readmePath: "README.md",
|
|
supportedPaths: {
|
|
localComposePath: "docker/docker-compose.yml",
|
|
deploymentConfigPath: "config.yaml.example",
|
|
runtimeDeploymentPath: "infra-cdk/lib/backend-stack.ts",
|
|
frontendDeploymentPath: "infra-cdk/lib/amplify-hosting-stack.ts",
|
|
terraformRuntimeDeploymentPath:
|
|
"infra-terraform/modules/backend/copilotkit_runtime.tf",
|
|
terraformReadmePath: "infra-terraform/README.md",
|
|
deployScriptPaths: ["deploy-langgraph.sh", "deploy-strands.sh"],
|
|
},
|
|
},
|
|
] as const satisfies readonly ManagedTemplateContract[];
|
|
|
|
/**
|
|
* Reads one required managed-template surface from its authoritative path.
|
|
*
|
|
* @param contract - Managed template and its exact surface paths.
|
|
* @param relativePath - Surface path relative to the integration directory.
|
|
* @param surface - Human-readable surface name used in assertion failures.
|
|
* @returns The UTF-8 contents, or an empty string after a missing-file assertion.
|
|
*/
|
|
function readManagedSurface(
|
|
contract: ManagedTemplateContract,
|
|
relativePath: string,
|
|
surface: string,
|
|
): string {
|
|
const filePath = path.join(integrationsDir, contract.directory, relativePath);
|
|
const exists = fs.existsSync(filePath);
|
|
|
|
expect(
|
|
exists,
|
|
`${contract.directory} ${surface} must exist at ${relativePath}`,
|
|
).toBe(true);
|
|
|
|
return exists ? fs.readFileSync(filePath, "utf8") : "";
|
|
}
|
|
|
|
/**
|
|
* Reads one local Intelligence stack surface.
|
|
*
|
|
* @param integration - Integration directory name.
|
|
* @param relativePath - Surface path relative to that integration.
|
|
* @returns The UTF-8 file contents.
|
|
*/
|
|
function readIntelligenceDevSurface(
|
|
integration: string,
|
|
relativePath: string,
|
|
): string {
|
|
return fs.readFileSync(
|
|
path.join(integrationsDir, integration, relativePath),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads one optional local Intelligence stack surface.
|
|
*
|
|
* @param integration - Integration directory name.
|
|
* @param relativePath - Surface path relative to that integration.
|
|
* @returns The UTF-8 file contents, or an empty string when absent.
|
|
*/
|
|
function readOptionalIntelligenceDevSurface(
|
|
integration: string,
|
|
relativePath: string,
|
|
): string {
|
|
const filePath = path.join(integrationsDir, integration, relativePath);
|
|
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
}
|
|
|
|
/**
|
|
* Replaces the expected per-starter Compose differences with stable markers.
|
|
*
|
|
* @param compose - Local Intelligence Compose source.
|
|
* @returns The normalized source for drift checks.
|
|
*/
|
|
function normalizeIntelligenceComposeForDriftGuard(compose: string): string {
|
|
return compose
|
|
.replace(/^name: .+$/m, "name: <starter-project-name>")
|
|
.replace(
|
|
/\$\{POSTGRES_HOST_PORT:-\d+\}/g,
|
|
"${POSTGRES_HOST_PORT:-<postgres-host-port>}",
|
|
)
|
|
.replace(
|
|
/\$\{REDIS_HOST_PORT:-\d+\}/g,
|
|
"${REDIS_HOST_PORT:-<redis-host-port>}",
|
|
)
|
|
.replace(
|
|
/\$\{APP_API_HOST_PORT:-\d+\}/g,
|
|
"${APP_API_HOST_PORT:-<app-api-host-port>}",
|
|
)
|
|
.replace(
|
|
/\$\{GATEWAY_HOST_PORT:-\d+\}/g,
|
|
"${GATEWAY_HOST_PORT:-<gateway-host-port>}",
|
|
)
|
|
.replace(
|
|
/:[0-9]+ \(api\), :[0-9]+ \(gateway\)/g,
|
|
":<app-api-host-port> (api), :<gateway-host-port> (gateway)",
|
|
)
|
|
.replace(/:[0-9]+$/gm, (match) =>
|
|
match === ":5432" ||
|
|
match === ":6379" ||
|
|
match === ":4201" ||
|
|
match === ":4401"
|
|
? match
|
|
: ":<host-port>",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Asserts that Docker Compose accepts one local Intelligence stack.
|
|
*
|
|
* @param integration - Integration directory name.
|
|
*/
|
|
function expectDockerComposeConfigToSucceed(integration: string): void {
|
|
const composePath = path.join(
|
|
integrationsDir,
|
|
integration,
|
|
"docker-compose.intelligence.yml",
|
|
);
|
|
const result = spawnSync(
|
|
"docker",
|
|
["compose", "-f", composePath, "config", "--quiet"],
|
|
{
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
},
|
|
);
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
[
|
|
`${integration} docker-compose.intelligence.yml failed docker compose config --quiet`,
|
|
result.error ? `error: ${result.error.message}` : undefined,
|
|
result.stdout ? `stdout: ${result.stdout}` : undefined,
|
|
result.stderr ? `stderr: ${result.stderr}` : undefined,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns one two-space-indented YAML mapping section.
|
|
*
|
|
* @param contents - YAML source containing service-style mappings.
|
|
* @param name - Mapping key whose complete section should be returned.
|
|
* @returns The mapping section, or an empty string when it is absent.
|
|
*/
|
|
function yamlMappingSection(contents: string, name: string): string {
|
|
const lines = contents.split(/\r?\n/);
|
|
const start = lines.findIndex((line) => line === ` ${name}:`);
|
|
if (start < 0) {
|
|
return "";
|
|
}
|
|
|
|
const relativeEnd = lines
|
|
.slice(start + 1)
|
|
.findIndex((line) => /^ [^\s].*:\s*$/.test(line));
|
|
const end = relativeEnd < 0 ? lines.length : start + relativeEnd + 1;
|
|
return lines.slice(start, end).join("\n");
|
|
}
|
|
|
|
/** Matches a service-level reference to the generated project's root env. */
|
|
function rootManagedEnvFilePattern(): RegExp {
|
|
return /env_file:\s*(?:\r?\n\s*-\s*)?\.\.\/\.env\b/;
|
|
}
|
|
|
|
/**
|
|
* Matches an environment identifier without matching it inside a longer name.
|
|
*
|
|
* @param identifier - Exact uppercase environment identifier to match.
|
|
* @returns A pattern that excludes surrounding uppercase identifier characters.
|
|
*/
|
|
function exactEnvIdentifierPattern(identifier: string): RegExp {
|
|
return new RegExp(`(^|[^A-Z0-9_])${identifier}([^A-Z0-9_]|$)`);
|
|
}
|
|
|
|
/** Returns whether a node is the `process.env` object expression. */
|
|
function isProcessEnvObject(node: ts.Node): boolean {
|
|
return (
|
|
ts.isPropertyAccessExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
node.expression.text === "process" &&
|
|
node.name.text === "env"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether a node is a dot or bracket read from `process.env`.
|
|
*
|
|
* @param node - TypeScript syntax node under inspection.
|
|
* @param identifier - Exact environment identifier that must be read.
|
|
*/
|
|
function isProcessEnvRead(node: ts.Node, identifier: string): boolean {
|
|
if (ts.isPropertyAccessExpression(node)) {
|
|
return isProcessEnvObject(node.expression) && node.name.text === identifier;
|
|
}
|
|
|
|
return (
|
|
ts.isElementAccessExpression(node) &&
|
|
isProcessEnvObject(node.expression) &&
|
|
ts.isStringLiteral(node.argumentExpression) &&
|
|
node.argumentExpression.text === identifier
|
|
);
|
|
}
|
|
|
|
/** Returns the static text of an object-literal property name when available. */
|
|
function propertyNameText(name: ts.PropertyName): string | null {
|
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
return name.text;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Parses managed scaffold TypeScript and rejects parser-recovered source. */
|
|
function parseManagedSource(contents: string): ts.SourceFile {
|
|
const transpiled = ts.transpileModule(contents, {
|
|
compilerOptions: {
|
|
jsx: ts.JsxEmit.Preserve,
|
|
module: ts.ModuleKind.ESNext,
|
|
target: ts.ScriptTarget.Latest,
|
|
},
|
|
fileName: "managed-integration-contract.tsx",
|
|
reportDiagnostics: true,
|
|
});
|
|
const parseDiagnostics =
|
|
transpiled.diagnostics?.filter(
|
|
(diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
|
|
) ?? [];
|
|
expect(
|
|
parseDiagnostics,
|
|
"managed scaffold source must parse without diagnostics",
|
|
).toHaveLength(0);
|
|
|
|
const sourceFile = ts.createSourceFile(
|
|
"managed-integration-contract.tsx",
|
|
contents,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TSX,
|
|
);
|
|
|
|
return sourceFile;
|
|
}
|
|
|
|
/** Returns an expression without transparent TypeScript wrappers. */
|
|
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
if (
|
|
ts.isParenthesizedExpression(expression) ||
|
|
ts.isAsExpression(expression) ||
|
|
ts.isTypeAssertionExpression(expression) ||
|
|
ts.isSatisfiesExpression(expression) ||
|
|
ts.isNonNullExpression(expression)
|
|
) {
|
|
return unwrapExpression(expression.expression);
|
|
}
|
|
|
|
return expression;
|
|
}
|
|
|
|
/** Returns one exact object-literal property assignment when present. */
|
|
function objectPropertyAssignment(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
name: string,
|
|
): ts.PropertyAssignment | null {
|
|
for (const property of objectLiteral.properties) {
|
|
if (
|
|
ts.isPropertyAssignment(property) &&
|
|
propertyNameText(property.name) === name
|
|
) {
|
|
return property;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns a direct value or conditional spread that reads one env name. */
|
|
function objectEnvironmentExpression(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
name: string,
|
|
): ts.Expression | null {
|
|
const direct = objectPropertyAssignment(objectLiteral, name);
|
|
if (direct) return direct.initializer;
|
|
|
|
for (const property of objectLiteral.properties) {
|
|
if (
|
|
ts.isSpreadAssignment(property) &&
|
|
expressionContainsEnvRead(property.expression, name)
|
|
) {
|
|
return property.expression;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns whether one expression contains the exact managed env read. */
|
|
function expressionContainsEnvRead(
|
|
expression: ts.Expression,
|
|
identifier: string,
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits one initializer node looking for the required env read. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) {
|
|
return;
|
|
}
|
|
if (isProcessEnvRead(node, identifier)) {
|
|
found = true;
|
|
return;
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(expression);
|
|
return found;
|
|
}
|
|
|
|
/** Returns whether an expression contains one exact string literal. */
|
|
function expressionContainsStringLiteral(
|
|
expression: ts.Expression,
|
|
expected: string,
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits one initializer node looking for the required literal. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (
|
|
(ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) &&
|
|
node.text === expected
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(expression);
|
|
return found;
|
|
}
|
|
|
|
/** Returns whether an expression uses the managed key without telemetry. */
|
|
function expressionUsesManagedKeyWithoutTelemetry(
|
|
expression: ts.Expression,
|
|
): boolean {
|
|
return (
|
|
expressionContainsEnvRead(expression, MANAGED_API_KEY) &&
|
|
!expressionContainsEnvRead(expression, OPTIONAL_TELEMETRY_ID)
|
|
);
|
|
}
|
|
|
|
/** Returns whether an expression is a literal boolean string projection. */
|
|
function isBooleanStringProjection(expression: ts.Expression): boolean {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isStringLiteral(unwrapped)) {
|
|
return unwrapped.text === "true" || unwrapped.text === "false";
|
|
}
|
|
if (ts.isConditionalExpression(unwrapped)) {
|
|
return (
|
|
isBooleanStringProjection(unwrapped.whenTrue) &&
|
|
isBooleanStringProjection(unwrapped.whenFalse)
|
|
);
|
|
}
|
|
if (
|
|
ts.isCallExpression(unwrapped) &&
|
|
ts.isPropertyAccessExpression(unwrapped.expression) &&
|
|
ts.isIdentifier(unwrapped.expression.expression) &&
|
|
unwrapped.expression.expression.text === "JSON" &&
|
|
unwrapped.expression.name.text === "stringify" &&
|
|
unwrapped.arguments.length === 1
|
|
) {
|
|
return isBooleanStringProjection(unwrapped.arguments[0]!);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Returns whether the public gate is boolean-only and keyed by the CPK credential. */
|
|
function expressionUsesManagedBooleanGate(expression: ts.Expression): boolean {
|
|
return (
|
|
expressionContainsEnvRead(expression, MANAGED_API_KEY) &&
|
|
!expressionContainsEnvRead(expression, OPTIONAL_TELEMETRY_ID) &&
|
|
isBooleanStringProjection(expression)
|
|
);
|
|
}
|
|
|
|
/** Returns the expression assigned to one exact object-literal property. */
|
|
function objectPropertyExpression(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
name: string,
|
|
): ts.Expression | null {
|
|
for (const property of objectLiteral.properties) {
|
|
if (
|
|
ts.isPropertyAssignment(property) &&
|
|
propertyNameText(property.name) === name
|
|
) {
|
|
return property.initializer;
|
|
}
|
|
if (
|
|
ts.isShorthandPropertyAssignment(property) &&
|
|
property.name.text === name
|
|
) {
|
|
return property.name;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns whether the Runtime's Intelligence client owns the API-key read. */
|
|
function hasRuntimeApiKeyRead(sourceFile: ts.SourceFile): boolean {
|
|
const clientUsesManagedKey = (clientExpression: ts.Expression): boolean => {
|
|
const client = resolveTopLevelExpression(sourceFile, clientExpression);
|
|
if (
|
|
!ts.isNewExpression(client) ||
|
|
!ts.isIdentifier(client.expression) ||
|
|
client.expression.text !== "CopilotKitIntelligence"
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const options = client.arguments?.[0]
|
|
? unwrapExpression(client.arguments[0])
|
|
: null;
|
|
if (!options || !ts.isObjectLiteralExpression(options)) return false;
|
|
|
|
const apiKey = objectPropertyAssignment(options, "apiKey");
|
|
return Boolean(
|
|
apiKey &&
|
|
expressionUsesManagedKeyWithoutTelemetry(
|
|
resolveTopLevelExpression(sourceFile, apiKey.initializer),
|
|
),
|
|
);
|
|
};
|
|
|
|
return newExpressionOptions(sourceFile, "CopilotRuntime").some(
|
|
(runtimeOptions) => {
|
|
const directIntelligence = objectPropertyExpression(
|
|
runtimeOptions,
|
|
"intelligence",
|
|
);
|
|
if (directIntelligence && clientUsesManagedKey(directIntelligence)) {
|
|
return true;
|
|
}
|
|
|
|
let found = false;
|
|
const visit = (node: ts.Node): void => {
|
|
if (
|
|
ts.isNewExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
node.expression.text === "CopilotKitIntelligence"
|
|
) {
|
|
found = clientUsesManagedKey(node);
|
|
}
|
|
if (!found) ts.forEachChild(node, visit);
|
|
};
|
|
|
|
visit(runtimeOptions);
|
|
return found;
|
|
},
|
|
);
|
|
}
|
|
|
|
/** Returns the initializer for one top-level variable identifier. */
|
|
function topLevelVariableInitializer(
|
|
sourceFile: ts.SourceFile,
|
|
identifier: string,
|
|
): ts.Expression | null {
|
|
for (const statement of sourceFile.statements) {
|
|
if (!ts.isVariableStatement(statement)) {
|
|
continue;
|
|
}
|
|
|
|
for (const declaration of statement.declarationList.declarations) {
|
|
if (
|
|
ts.isIdentifier(declaration.name) &&
|
|
declaration.name.text === identifier
|
|
) {
|
|
return declaration.initializer ?? null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns every variable initializer with one exact identifier in a source tree. */
|
|
function variableInitializers(
|
|
root: ts.Node,
|
|
identifier: string,
|
|
): readonly ts.Expression[] {
|
|
const matches: ts.Expression[] = [];
|
|
|
|
/** Visits variable declarations for the requested identifier. */
|
|
function visit(node: ts.Node): void {
|
|
if (
|
|
ts.isVariableDeclaration(node) &&
|
|
ts.isIdentifier(node.name) &&
|
|
node.name.text === identifier &&
|
|
node.initializer
|
|
) {
|
|
matches.push(node.initializer);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(root);
|
|
return matches;
|
|
}
|
|
|
|
/** Resolves transparent wrappers and top-level variable aliases. */
|
|
function resolveTopLevelExpression(
|
|
sourceFile: ts.SourceFile,
|
|
expression: ts.Expression,
|
|
seenIdentifiers: ReadonlySet<string> = new Set(),
|
|
): ts.Expression {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (!ts.isIdentifier(unwrapped) || seenIdentifiers.has(unwrapped.text)) {
|
|
return unwrapped;
|
|
}
|
|
|
|
const initializer = topLevelVariableInitializer(sourceFile, unwrapped.text);
|
|
if (!initializer) return unwrapped;
|
|
|
|
return resolveTopLevelExpression(
|
|
sourceFile,
|
|
initializer,
|
|
new Set([...seenIdentifiers, unwrapped.text]),
|
|
);
|
|
}
|
|
|
|
/** Resolves one registered agent through an optional top-level identifier. */
|
|
function registeredAgentExpression(
|
|
sourceFile: ts.SourceFile,
|
|
expression: ts.Expression,
|
|
): ts.Expression {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (!ts.isIdentifier(unwrapped)) return unwrapped;
|
|
|
|
const initializer = topLevelVariableInitializer(sourceFile, unwrapped.text);
|
|
return initializer ? unwrapExpression(initializer) : unwrapped;
|
|
}
|
|
|
|
/** Returns whether an object property carries one exact identifier. */
|
|
function objectPropertyIsIdentifier(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
propertyName: string,
|
|
identifier: string,
|
|
): boolean {
|
|
return objectLiteral.properties.some((property) => {
|
|
if (
|
|
ts.isShorthandPropertyAssignment(property) &&
|
|
property.name.text === propertyName
|
|
) {
|
|
return property.name.text === identifier;
|
|
}
|
|
if (
|
|
ts.isPropertyAssignment(property) &&
|
|
propertyNameText(property.name) === propertyName
|
|
) {
|
|
const initializer = unwrapExpression(property.initializer);
|
|
return ts.isIdentifier(initializer) && initializer.text === identifier;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/** Resolves a default-exported object literal through top-level variables. */
|
|
function exportedObjectLiteral(
|
|
expression: ts.Expression,
|
|
sourceFile: ts.SourceFile,
|
|
seenIdentifiers: ReadonlySet<string> = new Set(),
|
|
): ts.ObjectLiteralExpression | null {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isObjectLiteralExpression(unwrapped)) {
|
|
return unwrapped;
|
|
}
|
|
if (ts.isIdentifier(unwrapped) && !seenIdentifiers.has(unwrapped.text)) {
|
|
const initializer = topLevelVariableInitializer(sourceFile, unwrapped.text);
|
|
if (initializer) {
|
|
return exportedObjectLiteral(
|
|
initializer,
|
|
sourceFile,
|
|
new Set([...seenIdentifiers, unwrapped.text]),
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns the source's default export expression when it has one. */
|
|
function defaultExportExpression(
|
|
sourceFile: ts.SourceFile,
|
|
): ts.Expression | null {
|
|
for (const statement of sourceFile.statements) {
|
|
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
return statement.expression;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns whether the exported Next thread gate owns the key read. */
|
|
function hasExportedGateApiKeyRead(sourceFile: ts.SourceFile): boolean {
|
|
const exported = defaultExportExpression(sourceFile);
|
|
if (!exported) {
|
|
return false;
|
|
}
|
|
|
|
const nextConfig = exportedObjectLiteral(exported, sourceFile);
|
|
return Boolean(
|
|
nextConfig &&
|
|
nestedPropertyMatches(
|
|
nextConfig,
|
|
"env",
|
|
NEXT_THREADS_GATE,
|
|
expressionUsesManagedBooleanGate,
|
|
),
|
|
);
|
|
}
|
|
|
|
/** Returns whether a nested object property satisfies an expression predicate. */
|
|
function nestedPropertyMatches(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
containerName: string,
|
|
propertyName: string,
|
|
predicate: (expression: ts.Expression) => boolean,
|
|
): boolean {
|
|
const container = objectPropertyAssignment(objectLiteral, containerName);
|
|
if (!container) {
|
|
return false;
|
|
}
|
|
const containerInitializer = unwrapExpression(container.initializer);
|
|
if (!ts.isObjectLiteralExpression(containerInitializer)) {
|
|
return false;
|
|
}
|
|
const property = objectPropertyAssignment(containerInitializer, propertyName);
|
|
return Boolean(property && predicate(property.initializer));
|
|
}
|
|
|
|
/** Returns the exact dotted parts of a property-access expression. */
|
|
function propertyAccessParts(expression: ts.Expression): readonly string[] {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return [unwrapped.text];
|
|
}
|
|
if (ts.isPropertyAccessExpression(unwrapped)) {
|
|
return [...propertyAccessParts(unwrapped.expression), unwrapped.name.text];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/** Returns whether an expression contains an exact dotted property path. */
|
|
function expressionContainsPropertyPath(
|
|
expression: ts.Expression,
|
|
expectedPath: readonly string[],
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits one expression node for the exact configured path. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (
|
|
ts.isExpression(node) &&
|
|
propertyAccessParts(node).join(".") === expectedPath.join(".")
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(expression);
|
|
return found;
|
|
}
|
|
|
|
/** Returns whether an expression resolves a value through Secrets Manager. */
|
|
function expressionContainsSecretResolution(
|
|
expression: ts.Expression,
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits one expression node for an explicit Secrets Manager call. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (ts.isCallExpression(node)) {
|
|
const callee = propertyAccessParts(node.expression);
|
|
if (
|
|
callee.at(-1) === "secretsManager" &&
|
|
callee.includes("SecretValue")
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(expression);
|
|
return found;
|
|
}
|
|
|
|
/** Returns all new-expression option objects matching a construct name and id. */
|
|
function constructOptions(
|
|
sourceFile: ts.SourceFile,
|
|
constructorPath: readonly string[],
|
|
constructId: string,
|
|
): readonly ts.ObjectLiteralExpression[] {
|
|
const matches: ts.ObjectLiteralExpression[] = [];
|
|
|
|
/** Visits construct calls for exact constructor and id ownership. */
|
|
function visit(node: ts.Node): void {
|
|
if (
|
|
ts.isNewExpression(node) &&
|
|
propertyAccessParts(node.expression).join(".") ===
|
|
constructorPath.join(".") &&
|
|
node.arguments?.[1] &&
|
|
ts.isStringLiteral(node.arguments[1]) &&
|
|
node.arguments[1].text === constructId
|
|
) {
|
|
const options = node.arguments[2]
|
|
? unwrapExpression(node.arguments[2])
|
|
: null;
|
|
if (options && ts.isObjectLiteralExpression(options)) {
|
|
matches.push(options);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return matches;
|
|
}
|
|
|
|
/** Returns every object-literal options argument for one constructor. */
|
|
function newExpressionOptions(
|
|
sourceFile: ts.SourceFile,
|
|
constructorName: string,
|
|
): readonly ts.ObjectLiteralExpression[] {
|
|
const matches: ts.ObjectLiteralExpression[] = [];
|
|
|
|
/** Visits constructor calls for exact constructor ownership. */
|
|
function visit(node: ts.Node): void {
|
|
if (
|
|
ts.isNewExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
node.expression.text === constructorName
|
|
) {
|
|
const options = node.arguments?.[0]
|
|
? unwrapExpression(node.arguments[0])
|
|
: null;
|
|
if (options && ts.isObjectLiteralExpression(options)) {
|
|
matches.push(options);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return matches;
|
|
}
|
|
|
|
/** Returns one constructor's object-literal options from an exact expression. */
|
|
function constructorOptionsFromExpression(
|
|
expression: ts.Expression,
|
|
constructorName: string,
|
|
): ts.ObjectLiteralExpression | null {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (
|
|
!ts.isNewExpression(unwrapped) ||
|
|
!ts.isIdentifier(unwrapped.expression) ||
|
|
unwrapped.expression.text !== constructorName
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const options = unwrapped.arguments?.[0]
|
|
? unwrapExpression(unwrapped.arguments[0])
|
|
: null;
|
|
return options && ts.isObjectLiteralExpression(options) ? options : null;
|
|
}
|
|
|
|
/** Returns whether a source contains a call to one exact function or method. */
|
|
function sourceContainsCall(
|
|
sourceFile: ts.SourceFile,
|
|
calleePath: readonly string[],
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits call expressions until the required callee is found. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
propertyAccessParts(node.expression).join(".") === calleePath.join(".")
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return found;
|
|
}
|
|
|
|
/** Returns calls to one exact function or method. */
|
|
function sourceCalls(
|
|
sourceFile: ts.SourceFile,
|
|
calleePath: readonly string[],
|
|
): readonly ts.CallExpression[] {
|
|
const matches: ts.CallExpression[] = [];
|
|
|
|
/** Visits source nodes for the required callee. */
|
|
function visit(node: ts.Node): void {
|
|
if (
|
|
ts.isCallExpression(node) &&
|
|
propertyAccessParts(node.expression).join(".") === calleePath.join(".")
|
|
) {
|
|
matches.push(node);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return matches;
|
|
}
|
|
|
|
/** Returns whether source contains an exact environment read. */
|
|
function sourceContainsEnvRead(
|
|
sourceFile: ts.SourceFile,
|
|
identifier: string,
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits source nodes for the required environment read. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (isProcessEnvRead(node, identifier)) {
|
|
found = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return found;
|
|
}
|
|
|
|
/** Returns the exported HTTP methods assigned to `handle(app)`. */
|
|
function exportedEndpointHandlers(
|
|
sourceFile: ts.SourceFile,
|
|
): ReadonlySet<string> {
|
|
const methods = new Set<string>();
|
|
|
|
for (const statement of sourceFile.statements) {
|
|
if (
|
|
!ts.isVariableStatement(statement) ||
|
|
!statement.modifiers?.some(
|
|
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
for (const declaration of statement.declarationList.declarations) {
|
|
const initializer = declaration.initializer
|
|
? unwrapExpression(declaration.initializer)
|
|
: null;
|
|
if (
|
|
ts.isIdentifier(declaration.name) &&
|
|
initializer &&
|
|
ts.isCallExpression(initializer) &&
|
|
ts.isIdentifier(initializer.expression) &&
|
|
initializer.expression.text === "handle" &&
|
|
initializer.arguments.length === 1 &&
|
|
ts.isIdentifier(initializer.arguments[0]) &&
|
|
initializer.arguments[0].text === "app"
|
|
) {
|
|
methods.add(declaration.name.text);
|
|
}
|
|
}
|
|
}
|
|
|
|
return methods;
|
|
}
|
|
|
|
/** Returns static JSX tag text when the tag is not computed. */
|
|
function jsxTagNameText(name: ts.JsxTagNameExpression): string | null {
|
|
if (ts.isIdentifier(name)) {
|
|
return name.text;
|
|
}
|
|
if (ts.isPropertyAccessExpression(name)) {
|
|
return propertyAccessParts(name).join(".");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns all JSX elements and self-closing elements with one tag name. */
|
|
function jsxNodesWithTag(
|
|
root: ts.Node,
|
|
tagName: string,
|
|
): readonly (ts.JsxElement | ts.JsxSelfClosingElement)[] {
|
|
const matches: (ts.JsxElement | ts.JsxSelfClosingElement)[] = [];
|
|
|
|
/** Visits JSX nodes for the exact tag name. */
|
|
function visit(node: ts.Node): void {
|
|
if (
|
|
ts.isJsxElement(node) &&
|
|
jsxTagNameText(node.openingElement.tagName) === tagName
|
|
) {
|
|
matches.push(node);
|
|
} else if (
|
|
ts.isJsxSelfClosingElement(node) &&
|
|
jsxTagNameText(node.tagName) === tagName
|
|
) {
|
|
matches.push(node);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(root);
|
|
return matches;
|
|
}
|
|
|
|
/** Returns the opening-like node for a JSX element. */
|
|
function jsxOpeningLikeElement(
|
|
node: ts.JsxElement | ts.JsxSelfClosingElement,
|
|
): ts.JsxOpeningLikeElement {
|
|
return ts.isJsxElement(node) ? node.openingElement : node;
|
|
}
|
|
|
|
/** Returns one exact JSX attribute. */
|
|
function jsxAttribute(
|
|
node: ts.JsxElement | ts.JsxSelfClosingElement,
|
|
attributeName: string,
|
|
): ts.JsxAttribute | null {
|
|
const opening = jsxOpeningLikeElement(node);
|
|
for (const property of opening.attributes.properties) {
|
|
if (
|
|
ts.isJsxAttribute(property) &&
|
|
property.name.getText() === attributeName
|
|
) {
|
|
return property;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns a stable structural identity for one JSX attribute value. */
|
|
function jsxAttributeIdentity(
|
|
attribute: ts.JsxAttribute | null,
|
|
): string | null {
|
|
if (!attribute?.initializer) {
|
|
return attribute ? "true" : null;
|
|
}
|
|
if (ts.isStringLiteral(attribute.initializer)) {
|
|
return JSON.stringify(attribute.initializer.text);
|
|
}
|
|
if (
|
|
ts.isJsxExpression(attribute.initializer) &&
|
|
attribute.initializer.expression
|
|
) {
|
|
return attribute.initializer.expression.getText();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Returns whether a JSX attribute is the boolean literal `false`. */
|
|
function jsxAttributeIsFalse(attribute: ts.JsxAttribute | null): boolean {
|
|
return Boolean(
|
|
attribute?.initializer &&
|
|
ts.isJsxExpression(attribute.initializer) &&
|
|
attribute.initializer.expression?.kind === ts.SyntaxKind.FalseKeyword,
|
|
);
|
|
}
|
|
|
|
/** Returns whether an object property is one exact string literal. */
|
|
function objectPropertyIsString(
|
|
objectLiteral: ts.ObjectLiteralExpression,
|
|
propertyName: string,
|
|
expected: string,
|
|
): boolean {
|
|
const property = objectPropertyAssignment(objectLiteral, propertyName);
|
|
if (!property) return false;
|
|
const initializer = unwrapExpression(property.initializer);
|
|
return ts.isStringLiteral(initializer) && initializer.text === expected;
|
|
}
|
|
|
|
/**
|
|
* Matches a documented environment assignment, including commented examples.
|
|
*
|
|
* @param identifier - Exact environment identifier expected before `=`.
|
|
* @returns A multiline assignment pattern.
|
|
*/
|
|
function envAssignmentPattern(identifier: string): RegExp {
|
|
return new RegExp(`^\\s*#?\\s*${identifier}\\s*=`, "m");
|
|
}
|
|
|
|
/** Returns every blank-line-delimited block in block-oriented text. */
|
|
function textBlocks(contents: string): readonly string[] {
|
|
return contents.split(/\r?\n\s*\r?\n/);
|
|
}
|
|
|
|
/** Returns the first blank-line-delimited text block containing a marker. */
|
|
function textBlockContaining(contents: string, marker: string): string {
|
|
return textBlocks(contents).find((block) => block.includes(marker)) ?? "";
|
|
}
|
|
|
|
/** Asserts telemetry copy describes identity without prerequisite semantics. */
|
|
function expectTelemetryIdentityDocumentation(contents: string): void {
|
|
const telemetryBlock = textBlockContaining(contents, OPTIONAL_TELEMETRY_ID);
|
|
|
|
expect(telemetryBlock).toMatch(/\boptional\b/i);
|
|
expect(telemetryBlock).toMatch(/\bnon[- ]?secret\b/i);
|
|
expect(telemetryBlock).toMatch(/\banalytics\b/i);
|
|
expect(telemetryBlock).toMatch(/\bidentity\b/i);
|
|
expect(telemetryBlock).not.toMatch(
|
|
/\b(?:auth(?:entication|orization)?|entitlements?|enabl(?:e[sd]?|ing|ement)|consent(?:s|ed|ing)?|send(?:s|ing)?|sent|transmit(?:s|ted|ting)?)\b/i,
|
|
);
|
|
}
|
|
|
|
/** Returns whether an env block has an explicit comment label. */
|
|
function envBlockHasLabel(block: string, label: RegExp): boolean {
|
|
return block
|
|
.split(/\r?\n/)
|
|
.some((line) => /^\s*#/.test(line) && label.test(line));
|
|
}
|
|
|
|
interface ManagedEnvSections {
|
|
readonly apiKey: string;
|
|
readonly telemetry: string;
|
|
}
|
|
|
|
/** Returns the exact ordered managed dotenv sections from the public contract. */
|
|
function managedEnvSections(contents: string): ManagedEnvSections | null {
|
|
const lines = contents.replaceAll("\r\n", "\n").split("\n");
|
|
const apiHeader = "# Your CopilotKit Intelligence API Key";
|
|
const telemetryHeader = "# CopilotKit Telemetry ID";
|
|
if (
|
|
lines.filter((line) => line === apiHeader).length !== 1 ||
|
|
lines.filter((line) => line === telemetryHeader).length !== 1 ||
|
|
lines.filter((line) => envAssignmentPattern(MANAGED_API_KEY).test(line))
|
|
.length !== 1 ||
|
|
lines.filter((line) =>
|
|
envAssignmentPattern(OPTIONAL_TELEMETRY_ID).test(line),
|
|
).length !== 1
|
|
) {
|
|
return null;
|
|
}
|
|
const apiHeaderIndex = lines.findIndex((line) => line === apiHeader);
|
|
if (apiHeaderIndex < 0) return null;
|
|
|
|
const projectName = lines[apiHeaderIndex + 1] ?? "";
|
|
const apiKeyAssignment = lines[apiHeaderIndex + 2] ?? "";
|
|
const separator = lines[apiHeaderIndex + 3];
|
|
const telemetryHeaderIndex = apiHeaderIndex + 4;
|
|
if (
|
|
!/^# Project Name: \S.*$/u.test(projectName) ||
|
|
!envAssignmentPattern(MANAGED_API_KEY).test(apiKeyAssignment) ||
|
|
separator !== "" ||
|
|
lines[telemetryHeaderIndex] !== telemetryHeader
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
let telemetryEnd = telemetryHeaderIndex + 1;
|
|
while (telemetryEnd < lines.length && lines[telemetryEnd] !== "") {
|
|
telemetryEnd += 1;
|
|
}
|
|
const telemetry = lines.slice(telemetryHeaderIndex, telemetryEnd).join("\n");
|
|
if (!envAssignmentPattern(OPTIONAL_TELEMETRY_ID).test(telemetry)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
apiKey: lines.slice(apiHeaderIndex, apiHeaderIndex + 3).join("\n"),
|
|
telemetry,
|
|
};
|
|
}
|
|
|
|
/** Asserts every env license occurrence has a deployment-mode label. */
|
|
function expectEnvLicenseOccurrencesClassified(contents: string): void {
|
|
const licenseBlocks = textBlocks(contents).filter(
|
|
(block) =>
|
|
block.includes(MANAGED_LICENSE_TOKEN) ||
|
|
LICENSE_GATING_LANGUAGE.test(block),
|
|
);
|
|
|
|
expect(
|
|
licenseBlocks.every((block) =>
|
|
envBlockHasLabel(block, SELF_HOSTED_OR_OFFLINE_LABEL),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
|
|
/** Returns Markdown paragraph starts containing license literals or gating copy. */
|
|
function markdownLicenseOccurrenceLines(lines: readonly string[]): number[] {
|
|
const occurrences: number[] = [];
|
|
let start = 0;
|
|
while (start < lines.length) {
|
|
while (start < lines.length && lines[start]?.trim() === "") start += 1;
|
|
if (start >= lines.length) break;
|
|
let end = start + 1;
|
|
while (end < lines.length && lines[end]?.trim() !== "") end += 1;
|
|
const block = lines.slice(start, end).join("\n");
|
|
if (
|
|
block.includes(MANAGED_LICENSE_TOKEN) ||
|
|
LICENSE_GATING_LANGUAGE.test(block)
|
|
) {
|
|
occurrences.push(start);
|
|
}
|
|
start = end + 1;
|
|
}
|
|
|
|
return occurrences;
|
|
}
|
|
|
|
interface MarkdownHeadingSection {
|
|
readonly heading: string;
|
|
readonly contents: string;
|
|
}
|
|
|
|
/** Returns every Markdown heading section through its next peer heading. */
|
|
function markdownHeadingSections(
|
|
markdown: string,
|
|
): readonly MarkdownHeadingSection[] {
|
|
const lines = markdown.split(/\r?\n/);
|
|
const sections: MarkdownHeadingSection[] = [];
|
|
|
|
for (let start = 0; start < lines.length; start += 1) {
|
|
const heading = lines[start]?.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
if (!heading) continue;
|
|
|
|
const headingLevel = heading[1]?.length ?? 0;
|
|
let end = lines.length;
|
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
const subsequent = lines[index]?.match(/^(#{1,6})\s+/);
|
|
if (subsequent && (subsequent[1]?.length ?? 0) <= headingLevel) {
|
|
end = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
sections.push({
|
|
heading: heading[2] ?? "",
|
|
contents: lines.slice(start, end).join("\n"),
|
|
});
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
/** Returns the managed Markdown section containing both managed fields. */
|
|
function managedMarkdownSection(markdown: string): string {
|
|
return (
|
|
markdownHeadingSections(markdown).find(
|
|
({ heading, contents }) =>
|
|
MANAGED_DOCUMENTATION_LABEL.test(heading) &&
|
|
exactEnvIdentifierPattern(MANAGED_API_KEY).test(contents) &&
|
|
exactEnvIdentifierPattern(OPTIONAL_TELEMETRY_ID).test(contents),
|
|
)?.contents ?? ""
|
|
);
|
|
}
|
|
|
|
/** Returns distinct offline or self-hosted license-token guidance. */
|
|
function offlineOrSelfHostedLicenseSection(markdown: string): string {
|
|
return (
|
|
markdownHeadingSections(markdown).find(
|
|
({ heading, contents }) =>
|
|
SELF_HOSTED_OR_OFFLINE_LABEL.test(heading) &&
|
|
(exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN).test(contents) ||
|
|
/\blicen[cs]e\s+token\b/i.test(contents)),
|
|
)?.contents ?? ""
|
|
);
|
|
}
|
|
|
|
/** Returns whether a Markdown line has a heading ancestor with the label. */
|
|
function markdownLineHasLabeledAncestor(
|
|
lines: readonly string[],
|
|
lineIndex: number,
|
|
label: RegExp,
|
|
): boolean {
|
|
let childLevel = 7;
|
|
for (let index = lineIndex; index >= 0; index -= 1) {
|
|
const heading = lines[index]?.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
const headingLevel = heading?.[1]?.length ?? 0;
|
|
if (heading && headingLevel < childLevel) {
|
|
if (label.test(heading[2] ?? "")) {
|
|
return true;
|
|
}
|
|
childLevel = headingLevel;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Asserts every README license mention has a deployment-mode heading. */
|
|
function expectMarkdownLicenseOccurrencesClassified(markdown: string): void {
|
|
const lines = markdown.split(/\r?\n/);
|
|
const licenseLines = markdownLicenseOccurrenceLines(lines);
|
|
|
|
expect(
|
|
licenseLines.every((index) =>
|
|
markdownLineHasLabeledAncestor(
|
|
lines,
|
|
index,
|
|
SELF_HOSTED_OR_OFFLINE_LABEL,
|
|
),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
|
|
/**
|
|
* Asserts the managed runtime reads only the managed project credential.
|
|
*
|
|
* @param contents - Runtime route or bridge source.
|
|
*/
|
|
function expectManagedRuntimeContract(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
|
|
expect(hasRuntimeApiKeyRead(sourceFile)).toBe(true);
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_API_KEY));
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_TELEMETRY_ID));
|
|
}
|
|
|
|
/**
|
|
* Asserts the browser-safe feature gate follows the managed project credential.
|
|
*
|
|
* @param contents - Next.js or Vite gate configuration source.
|
|
*/
|
|
function expectManagedGateContract(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
|
|
expect(hasExportedGateApiKeyRead(sourceFile)).toBe(true);
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_API_KEY));
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_TELEMETRY_ID));
|
|
expect(contents).not.toContain(MANAGED_LICENSE_TOKEN);
|
|
}
|
|
|
|
/**
|
|
* Asserts an env example documents the managed key and optional telemetry ID.
|
|
*
|
|
* @param contents - Managed template environment example contents.
|
|
*/
|
|
function expectManagedEnvContract(contents: string): void {
|
|
const managedSections = managedEnvSections(contents);
|
|
|
|
expect(managedSections).not.toBeNull();
|
|
if (!managedSections) return;
|
|
expect(managedSections.apiKey).toMatch(envAssignmentPattern(MANAGED_API_KEY));
|
|
expectTelemetryIdentityDocumentation(managedSections.telemetry);
|
|
expect(contents).not.toMatch(envAssignmentPattern(LEGACY_API_KEY));
|
|
expect(contents).not.toMatch(envAssignmentPattern(LEGACY_TELEMETRY_ID));
|
|
expectEnvLicenseOccurrencesClassified(contents);
|
|
}
|
|
|
|
/**
|
|
* Asserts a README documents the managed key and optional telemetry identity.
|
|
*
|
|
* @param contents - Managed template README contents.
|
|
* @param requiresLicenseSection - Whether this template documents a separate license path.
|
|
*/
|
|
function expectManagedReadmeContract(
|
|
contents: string,
|
|
requiresLicenseSection = true,
|
|
): void {
|
|
const managedSection = managedMarkdownSection(contents);
|
|
const offlineOrSelfHostedSection =
|
|
offlineOrSelfHostedLicenseSection(contents);
|
|
|
|
expect(managedSection).not.toBe("");
|
|
expect(managedSection).not.toContain(MANAGED_LICENSE_TOKEN);
|
|
expectTelemetryIdentityDocumentation(managedSection);
|
|
if (requiresLicenseSection) {
|
|
expect(offlineOrSelfHostedSection).not.toBe("");
|
|
expect(managedSection).not.toContain(offlineOrSelfHostedSection);
|
|
} else {
|
|
expect(offlineOrSelfHostedSection).toBe("");
|
|
}
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_API_KEY));
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_TELEMETRY_ID));
|
|
expectMarkdownLicenseOccurrencesClassified(contents);
|
|
}
|
|
|
|
/** Reads the Runtime and React pins that own one template's entitlement flow. */
|
|
function readManagedSdkPins(
|
|
contract: ManagedTemplateContract,
|
|
): readonly ManagedSdkPin[] {
|
|
const packageContracts =
|
|
contract.directory === "agentcore"
|
|
? [
|
|
{
|
|
packageName: "@copilotkit/runtime" as const,
|
|
packagePath: "infra-cdk/lambdas/copilotkit-runtime/package.json",
|
|
},
|
|
{
|
|
packageName: "@copilotkit/react-core" as const,
|
|
packagePath: "frontend/package.json",
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
packageName: "@copilotkit/runtime" as const,
|
|
packagePath: "package.json",
|
|
},
|
|
{
|
|
packageName: "@copilotkit/react-core" as const,
|
|
packagePath: "package.json",
|
|
},
|
|
];
|
|
|
|
return packageContracts.map(({ packageName, packagePath }) => {
|
|
const manifest = JSON.parse(
|
|
readManagedSurface(contract, packagePath, `${packageName} manifest`),
|
|
) as {
|
|
readonly dependencies?: Readonly<Record<string, string>>;
|
|
readonly devDependencies?: Readonly<Record<string, string>>;
|
|
};
|
|
const version =
|
|
manifest.dependencies?.[packageName] ??
|
|
manifest.devDependencies?.[packageName] ??
|
|
"";
|
|
|
|
expect(
|
|
version,
|
|
`${contract.directory} must pin ${packageName} in ${packagePath}`,
|
|
).toMatch(/^\d+\.\d+\.\d+$/u);
|
|
|
|
return { packageName, packagePath, version };
|
|
});
|
|
}
|
|
|
|
/** Returns whether a classified package pin supports managed entitlements. */
|
|
function pinSupportsManagedEntitlements(pin: ManagedSdkPin): boolean {
|
|
const classifications = MANAGED_ENTITLEMENT_CONTRACT_BY_SDK_VERSION[
|
|
pin.packageName
|
|
] as Readonly<Record<string, boolean>>;
|
|
const supportsManagedEntitlements = classifications[pin.version];
|
|
|
|
expect(
|
|
supportsManagedEntitlements,
|
|
`${pin.packageName}@${pin.version} must be classified before a managed template can use it`,
|
|
).not.toBeUndefined();
|
|
|
|
return supportsManagedEntitlements === true;
|
|
}
|
|
|
|
/** Asserts one ordinary template retains its framework-specific Runtime agent. */
|
|
function expectRuntimeAgentContract(
|
|
contents: string,
|
|
contract: RuntimeAgentContract,
|
|
agentContents: string = contents,
|
|
): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const runtimeOptions = newExpressionOptions(sourceFile, "CopilotRuntime");
|
|
|
|
expect(runtimeOptions).toHaveLength(1);
|
|
if (runtimeOptions.length !== 1) return;
|
|
const agents = objectPropertyAssignment(runtimeOptions[0]!, "agents");
|
|
expect(agents).not.toBeNull();
|
|
if (!agents) return;
|
|
const registeredAgents = unwrapExpression(agents.initializer);
|
|
|
|
if (contract.registration === "factory") {
|
|
expect(ts.isCallExpression(registeredAgents)).toBe(true);
|
|
if (!ts.isCallExpression(registeredAgents)) return;
|
|
const registeredCallee = propertyAccessParts(registeredAgents.expression);
|
|
const factoryCall =
|
|
registeredCallee.length === 1 &&
|
|
registeredCallee[0] === "createLocalAgents"
|
|
? sourceCalls(parseManagedSource(agentContents), contract.calleePath)[0]
|
|
: registeredAgents;
|
|
expect(factoryCall).not.toBeUndefined();
|
|
if (!factoryCall) return;
|
|
expect(propertyAccessParts(factoryCall.expression)).toEqual(
|
|
contract.calleePath,
|
|
);
|
|
const argument = factoryCall.arguments[0]
|
|
? unwrapExpression(factoryCall.arguments[0])
|
|
: null;
|
|
expect(argument && ts.isObjectLiteralExpression(argument)).toBe(true);
|
|
if (!argument || !ts.isObjectLiteralExpression(argument)) return;
|
|
expect(
|
|
objectPropertyIsIdentifier(
|
|
argument,
|
|
contract.argumentIdentifier,
|
|
contract.argumentIdentifier,
|
|
),
|
|
).toBe(true);
|
|
return;
|
|
}
|
|
|
|
expect(ts.isObjectLiteralExpression(registeredAgents)).toBe(true);
|
|
if (!ts.isObjectLiteralExpression(registeredAgents)) return;
|
|
const defaultAgent = objectPropertyAssignment(registeredAgents, "default");
|
|
expect(defaultAgent).not.toBeNull();
|
|
if (!defaultAgent) return;
|
|
const agent = registeredAgentExpression(sourceFile, defaultAgent.initializer);
|
|
let agentOptions: ts.ObjectLiteralExpression | null;
|
|
if (
|
|
ts.isCallExpression(agent) &&
|
|
propertyAccessParts(agent.expression).join(".") === "createDefaultAgent"
|
|
) {
|
|
const factoryAgentOptions = newExpressionOptions(
|
|
parseManagedSource(agentContents),
|
|
contract.constructorName,
|
|
);
|
|
expect(factoryAgentOptions).toHaveLength(1);
|
|
agentOptions = factoryAgentOptions[0] ?? null;
|
|
} else {
|
|
expect(ts.isNewExpression(agent)).toBe(true);
|
|
if (!ts.isNewExpression(agent)) return;
|
|
expect(propertyAccessParts(agent.expression)).toEqual([
|
|
contract.constructorName,
|
|
]);
|
|
const inlineAgentOptions = agent.arguments?.[0]
|
|
? unwrapExpression(agent.arguments[0])
|
|
: null;
|
|
expect(
|
|
inlineAgentOptions && ts.isObjectLiteralExpression(inlineAgentOptions),
|
|
).toBe(true);
|
|
if (
|
|
!inlineAgentOptions ||
|
|
!ts.isObjectLiteralExpression(inlineAgentOptions)
|
|
) {
|
|
return;
|
|
}
|
|
agentOptions = inlineAgentOptions;
|
|
}
|
|
|
|
expect(agentOptions && ts.isObjectLiteralExpression(agentOptions)).toBe(true);
|
|
if (!agentOptions || !ts.isObjectLiteralExpression(agentOptions)) return;
|
|
|
|
for (const optionContract of contract.options) {
|
|
const option = objectPropertyAssignment(
|
|
agentOptions,
|
|
optionContract.property,
|
|
);
|
|
expect(option).not.toBeNull();
|
|
if (!option) continue;
|
|
for (const identifier of optionContract.environmentReads ?? []) {
|
|
expect(expressionContainsEnvRead(option.initializer, identifier)).toBe(
|
|
true,
|
|
);
|
|
}
|
|
for (const literal of optionContract.stringLiterals ?? []) {
|
|
expect(expressionContainsStringLiteral(option.initializer, literal)).toBe(
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Asserts a Next/Hono runtime preserves its complete REST handler surface. */
|
|
function expectEndpointHandlerContract(contents: string): void {
|
|
const handlers = exportedEndpointHandlers(parseManagedSource(contents));
|
|
|
|
expect([...handlers].sort()).toEqual(["DELETE", "GET", "PATCH", "POST"]);
|
|
}
|
|
|
|
/** Asserts a frontend preserves REST transport and shared thread ownership. */
|
|
function expectFrontendThreadContract(
|
|
providerContents: string,
|
|
threadContents: string,
|
|
): void {
|
|
const providerSource = parseManagedSource(providerContents);
|
|
const threadSource = parseManagedSource(threadContents);
|
|
const providers = [
|
|
...jsxNodesWithTag(providerSource, "CopilotKit"),
|
|
...jsxNodesWithTag(providerSource, "CopilotKitProvider"),
|
|
];
|
|
const configurationProviders = jsxNodesWithTag(
|
|
threadSource,
|
|
"CopilotChatConfigurationProvider",
|
|
);
|
|
const drawers = jsxNodesWithTag(threadSource, "CopilotThreadsDrawer");
|
|
|
|
expect(providers).toHaveLength(1);
|
|
expect(configurationProviders).toHaveLength(1);
|
|
expect(drawers).toHaveLength(1);
|
|
if (
|
|
providers.length !== 1 ||
|
|
configurationProviders.length !== 1 ||
|
|
drawers.length !== 1
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const provider = providers[0]!;
|
|
const configurationProvider = configurationProviders[0]!;
|
|
const drawer = drawers[0]!;
|
|
const configuredAgent = jsxAttributeIdentity(
|
|
jsxAttribute(configurationProvider, "agentId"),
|
|
);
|
|
const drawerAgent = jsxAttributeIdentity(jsxAttribute(drawer, "agentId"));
|
|
|
|
expect(jsxAttribute(provider, "runtimeUrl")).not.toBeNull();
|
|
expect(jsxAttributeIsFalse(jsxAttribute(provider, "useSingleEndpoint"))).toBe(
|
|
true,
|
|
);
|
|
expect(jsxAttribute(configurationProvider, "threadId")).toBeNull();
|
|
expect(configuredAgent).not.toBeNull();
|
|
expect(drawerAgent).toBe(configuredAgent);
|
|
expect(
|
|
jsxNodesWithTag(configurationProvider, "CopilotThreadsDrawer"),
|
|
).toHaveLength(1);
|
|
|
|
const configuredChatCount = [
|
|
"CopilotChat",
|
|
"CopilotSidebar",
|
|
"Chat",
|
|
"ResearchAssistant",
|
|
].reduce(
|
|
(count, tagName) =>
|
|
count + jsxNodesWithTag(configurationProvider, tagName).length,
|
|
0,
|
|
);
|
|
expect(configuredChatCount).toBeGreaterThan(0);
|
|
}
|
|
|
|
/** Asserts MCP Apps retains its client middleware configuration. */
|
|
function expectMcpAppsRuntimeBehavior(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const middlewareOptions = newExpressionOptions(
|
|
sourceFile,
|
|
"MCPAppsMiddleware",
|
|
);
|
|
const agentOptions = newExpressionOptions(sourceFile, "BuiltInAgent");
|
|
|
|
expect(middlewareOptions).toHaveLength(1);
|
|
expect(agentOptions).toHaveLength(1);
|
|
expect(sourceContainsCall(sourceFile, ["agent", "use"])).toBe(true);
|
|
if (middlewareOptions.length !== 1) return;
|
|
|
|
const serversProperty = objectPropertyAssignment(
|
|
middlewareOptions[0]!,
|
|
"mcpServers",
|
|
);
|
|
const servers = serversProperty
|
|
? unwrapExpression(serversProperty.initializer)
|
|
: null;
|
|
expect(servers && ts.isArrayLiteralExpression(servers)).toBe(true);
|
|
if (!servers || !ts.isArrayLiteralExpression(servers)) return;
|
|
|
|
const configuredServers = servers.elements.filter(
|
|
ts.isObjectLiteralExpression,
|
|
);
|
|
expect(configuredServers).toHaveLength(1);
|
|
if (configuredServers.length !== 1) return;
|
|
expect(objectPropertyIsString(configuredServers[0]!, "type", "http")).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
objectPropertyIsString(configuredServers[0]!, "serverId", "threejs"),
|
|
).toBe(true);
|
|
expect(
|
|
objectPropertyIsString(
|
|
configuredServers[0]!,
|
|
"url",
|
|
"http://localhost:3108/mcp",
|
|
),
|
|
).toBe(true);
|
|
}
|
|
|
|
/** Asserts the bundled MCP server retains its tools and UI resource. */
|
|
function expectMcpServerBehavior(
|
|
serverContents: string,
|
|
transportContents: string,
|
|
): void {
|
|
const serverSource = parseManagedSource(serverContents);
|
|
const transportSource = parseManagedSource(transportContents);
|
|
const appToolCalls = sourceCalls(serverSource, ["registerAppTool"]);
|
|
const toolCalls = sourceCalls(serverSource, ["server", "registerTool"]);
|
|
const resourceCalls = sourceCalls(serverSource, ["registerAppResource"]);
|
|
const routeCalls = sourceCalls(transportSource, ["app", "all"]);
|
|
|
|
expect(appToolCalls).toHaveLength(1);
|
|
expect(toolCalls).toHaveLength(1);
|
|
expect(resourceCalls).toHaveLength(1);
|
|
expect(routeCalls).toHaveLength(1);
|
|
expect(appToolCalls[0]?.arguments[1]).toMatchObject({
|
|
text: "show_threejs_scene",
|
|
});
|
|
expect(toolCalls[0]?.arguments[0]).toMatchObject({ text: "learn_threejs" });
|
|
expect(sourceContainsCall(serverSource, ["startServer"])).toBe(true);
|
|
expect(sourceContainsCall(transportSource, ["server", "connect"])).toBe(true);
|
|
expect(
|
|
sourceContainsCall(transportSource, ["transport", "handleRequest"]),
|
|
).toBe(true);
|
|
expect(routeCalls[0]?.arguments[0]).toMatchObject({ text: "/mcp" });
|
|
|
|
const resourceArgument = resourceCalls[0]?.arguments[1];
|
|
expect(resourceArgument && ts.isIdentifier(resourceArgument)).toBe(true);
|
|
expect(serverContents).toContain(
|
|
'const resourceUri = "ui://threejs/mcp-app.html"',
|
|
);
|
|
}
|
|
|
|
/** Returns whether source declares one class extending the expected base. */
|
|
function sourceContainsClassExtension(
|
|
sourceFile: ts.SourceFile,
|
|
className: string,
|
|
baseName: string,
|
|
): boolean {
|
|
let found = false;
|
|
|
|
/** Visits class declarations for the exact inheritance edge. */
|
|
function visit(node: ts.Node): void {
|
|
if (found) return;
|
|
if (
|
|
ts.isClassDeclaration(node) &&
|
|
node.name?.text === className &&
|
|
node.heritageClauses?.some(
|
|
(clause) =>
|
|
clause.token === ts.SyntaxKind.ExtendsKeyword &&
|
|
clause.types.some(
|
|
(type) =>
|
|
ts.isIdentifier(type.expression) &&
|
|
type.expression.text === baseName,
|
|
),
|
|
)
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
return found;
|
|
}
|
|
|
|
/** Asserts A2A retains isolated multi-agent routing and URL ownership. */
|
|
function expectA2ARuntimeBehavior(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const runtimeOptions = newExpressionOptions(
|
|
sourceFile,
|
|
"RuntimeA2AMiddlewareAgent",
|
|
).find((options) => objectPropertyAssignment(options, "agentUrls"));
|
|
|
|
expect(
|
|
sourceContainsClassExtension(
|
|
sourceFile,
|
|
"RuntimeA2AMiddlewareAgent",
|
|
"A2AMiddlewareAgent",
|
|
),
|
|
).toBe(true);
|
|
expect(newExpressionOptions(sourceFile, "A2AMiddlewareAgent")).toHaveLength(
|
|
1,
|
|
);
|
|
expect(
|
|
newExpressionOptions(sourceFile, "HttpAgent").length,
|
|
).toBeGreaterThanOrEqual(2);
|
|
expect(sourceContainsCall(sourceFile, ["isolatedAgent", "setMessages"])).toBe(
|
|
true,
|
|
);
|
|
expect(sourceContainsCall(sourceFile, ["isolatedAgent", "runAgent"])).toBe(
|
|
true,
|
|
);
|
|
for (const identifier of [
|
|
"RESEARCH_AGENT_URL",
|
|
"ANALYSIS_AGENT_URL",
|
|
"ORCHESTRATOR_URL",
|
|
]) {
|
|
expect(sourceContainsEnvRead(sourceFile, identifier)).toBe(true);
|
|
}
|
|
|
|
expect(runtimeOptions).toBeDefined();
|
|
if (!runtimeOptions) return;
|
|
expect(objectPropertyIsString(runtimeOptions, "agentId", "a2a_chat")).toBe(
|
|
true,
|
|
);
|
|
const agentUrls = objectPropertyAssignment(runtimeOptions, "agentUrls");
|
|
const orchestrationAgentUrl = objectPropertyAssignment(
|
|
runtimeOptions,
|
|
"orchestrationAgentUrl",
|
|
);
|
|
expect(agentUrls?.initializer.getText()).toBe(
|
|
"[researchAgentUrl, analysisAgentUrl]",
|
|
);
|
|
expect(orchestrationAgentUrl?.initializer.getText()).toBe("orchestratorUrl");
|
|
}
|
|
|
|
/** Asserts A2A visualization stays registered inside the configured chat. */
|
|
function expectA2AVisualizationBehavior(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const toolCalls = sourceCalls(sourceFile, ["useFrontendTool"]);
|
|
const visualizationTool = toolCalls.find((call) => {
|
|
const options = call.arguments[0]
|
|
? unwrapExpression(call.arguments[0])
|
|
: null;
|
|
return (
|
|
options &&
|
|
ts.isObjectLiteralExpression(options) &&
|
|
objectPropertyIsString(options, "name", "send_message_to_a2a_agent")
|
|
);
|
|
});
|
|
|
|
expect(visualizationTool).toBeDefined();
|
|
expect(jsxNodesWithTag(sourceFile, "MessageToA2A")).toHaveLength(1);
|
|
expect(jsxNodesWithTag(sourceFile, "MessageFromA2A")).toHaveLength(1);
|
|
expect(jsxNodesWithTag(sourceFile, "CopilotChat")).toHaveLength(1);
|
|
expect(jsxNodesWithTag(sourceFile, "CopilotKit")).toHaveLength(0);
|
|
expect(jsxNodesWithTag(sourceFile, "CopilotKitProvider")).toHaveLength(0);
|
|
}
|
|
|
|
/** Asserts AgentCore selects managed Intelligence or its custom local runner. */
|
|
function expectAgentCoreRuntimeBehavior(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const endpointCalls = sourceCalls(sourceFile, ["createCopilotEndpoint"]);
|
|
const requiredAgentUrl = sourceCalls(sourceFile, ["requireEnv"]).some(
|
|
(call) =>
|
|
call.arguments[0] &&
|
|
ts.isStringLiteral(call.arguments[0]) &&
|
|
call.arguments[0].text === "AGENTCORE_AG_UI_URL",
|
|
);
|
|
|
|
expect(
|
|
sourceContainsClassExtension(
|
|
sourceFile,
|
|
"AgentCoreRunner",
|
|
"InMemoryAgentRunner",
|
|
),
|
|
).toBe(true);
|
|
expect(requiredAgentUrl).toBe(true);
|
|
expect(newExpressionOptions(sourceFile, "HttpAgent")).toHaveLength(1);
|
|
expect(newExpressionOptions(sourceFile, "MCPAppsMiddleware")).toHaveLength(1);
|
|
|
|
const runtimeInitializers = variableInitializers(sourceFile, "runtime");
|
|
expect(runtimeInitializers).toHaveLength(1);
|
|
const runtimeSelection = runtimeInitializers[0]
|
|
? unwrapExpression(runtimeInitializers[0])
|
|
: null;
|
|
expect(runtimeSelection && ts.isConditionalExpression(runtimeSelection)).toBe(
|
|
true,
|
|
);
|
|
if (!runtimeSelection || !ts.isConditionalExpression(runtimeSelection)) {
|
|
return;
|
|
}
|
|
|
|
expect(
|
|
expressionUsesManagedKeyWithoutTelemetry(runtimeSelection.condition),
|
|
).toBe(true);
|
|
const managedOptions = constructorOptionsFromExpression(
|
|
runtimeSelection.whenTrue,
|
|
"CopilotRuntime",
|
|
);
|
|
const localOptions = constructorOptionsFromExpression(
|
|
runtimeSelection.whenFalse,
|
|
"CopilotRuntime",
|
|
);
|
|
expect(managedOptions).not.toBeNull();
|
|
expect(localOptions).not.toBeNull();
|
|
if (!managedOptions || !localOptions) return;
|
|
|
|
expect(
|
|
objectPropertyExpression(managedOptions, "intelligence"),
|
|
).not.toBeNull();
|
|
expect(objectPropertyExpression(managedOptions, "licenseToken")).toBeNull();
|
|
expect(objectPropertyExpression(managedOptions, "runner")).toBeNull();
|
|
expect(objectPropertyExpression(localOptions, "intelligence")).toBeNull();
|
|
const localRunner = objectPropertyExpression(localOptions, "runner");
|
|
expect(localRunner).not.toBeNull();
|
|
const unwrappedLocalRunner = localRunner
|
|
? unwrapExpression(localRunner)
|
|
: null;
|
|
expect(
|
|
unwrappedLocalRunner &&
|
|
ts.isNewExpression(unwrappedLocalRunner) &&
|
|
ts.isIdentifier(unwrappedLocalRunner.expression) &&
|
|
unwrappedLocalRunner.expression.text === "AgentCoreRunner",
|
|
).toBe(true);
|
|
expect(endpointCalls).toHaveLength(1);
|
|
const endpointOptions = endpointCalls[0]?.arguments[0];
|
|
const unwrappedOptions = endpointOptions
|
|
? unwrapExpression(endpointOptions)
|
|
: null;
|
|
expect(
|
|
unwrappedOptions && ts.isObjectLiteralExpression(unwrappedOptions),
|
|
).toBe(true);
|
|
if (!unwrappedOptions || !ts.isObjectLiteralExpression(unwrappedOptions)) {
|
|
return;
|
|
}
|
|
expect(
|
|
objectPropertyIsString(unwrappedOptions, "basePath", "/copilotkit"),
|
|
).toBe(true);
|
|
}
|
|
|
|
/** Asserts AgentCore local services retain bridge routing and one network. */
|
|
function expectAgentCoreNetworkingBehavior(contents: string): void {
|
|
const agent = yamlMappingSection(contents, "agent");
|
|
const bridge = yamlMappingSection(contents, "bridge");
|
|
const frontend = yamlMappingSection(contents, "frontend");
|
|
|
|
expect(agent).toMatch(/networks:\s*\n\s*- agentcore-network/);
|
|
expect(bridge).toContain("AGENTCORE_AG_UI_URL=http://agent:8080/invocations");
|
|
expect(bridge).toMatch(
|
|
/depends_on:\s*\n\s*agent:\s*\n\s*condition: service_healthy/,
|
|
);
|
|
expect(bridge).toMatch(/networks:\s*\n\s*- agentcore-network/);
|
|
expect(frontend).toMatch(
|
|
/depends_on:\s*\n\s*bridge:\s*\n\s*condition: service_started/,
|
|
);
|
|
expect(frontend).toMatch(/networks:\s*\n\s*- agentcore-network/);
|
|
expect(contents).toMatch(
|
|
/^networks:\s*\n\s*agentcore-network:\s*\n\s*driver: bridge\s*$/m,
|
|
);
|
|
}
|
|
|
|
/** Asserts an AgentCore deploy script preserves one framework variant. */
|
|
function expectAgentCoreVariantBehavior(
|
|
contents: string,
|
|
pattern: string,
|
|
suffix: string,
|
|
): void {
|
|
expect(contents).toMatch(new RegExp(`^PATTERN="${pattern}"$`, "m"));
|
|
expect(contents).toMatch(new RegExp(`^SUFFIX="${suffix}"$`, "m"));
|
|
expect(contents).toMatch(/^CONFIG="\$SCRIPT_DIR\/config\.yaml"$/m);
|
|
expect(contents).toMatch(
|
|
/npx cdk@latest deploy --all --require-approval never/,
|
|
);
|
|
expect(contents).toMatch(
|
|
/uv run --project "\$SCRIPT_DIR" "\$SCRIPT_DIR\/scripts\/deploy-frontend\.py" "\$STACK_NAME"/,
|
|
);
|
|
}
|
|
|
|
/** Returns the provider and thread-owning frontend surfaces for a template. */
|
|
function frontendBehaviorPaths(contract: ManagedTemplateContract): {
|
|
readonly providerPath: string;
|
|
readonly threadPath: string;
|
|
} {
|
|
if (contract.directory === "agentcore") {
|
|
const chatPath = "frontend/src/components/chat/CopilotKit/index.tsx";
|
|
return { providerPath: chatPath, threadPath: chatPath };
|
|
}
|
|
if (contract.directory === "a2a-middleware") {
|
|
return { providerPath: "app/page.tsx", threadPath: "app/page.tsx" };
|
|
}
|
|
if (contract.directory === "mcp-apps") {
|
|
return { providerPath: "app/layout.tsx", threadPath: "app/page.tsx" };
|
|
}
|
|
|
|
return {
|
|
providerPath: "src/app/layout.tsx",
|
|
threadPath: "src/app/page.tsx",
|
|
};
|
|
}
|
|
|
|
/** Assert AgentCore local services consume the CLI-managed root env safely. */
|
|
function expectAgentCoreLocalComposeContract(contents: string): void {
|
|
const bridge = yamlMappingSection(contents, "bridge");
|
|
const frontend = yamlMappingSection(contents, "frontend");
|
|
|
|
expect(bridge).toMatch(rootManagedEnvFilePattern());
|
|
expect(frontend).toMatch(rootManagedEnvFilePattern());
|
|
expect(frontend).not.toContain(VITE_THREADS_GATE);
|
|
expect(frontend).not.toContain(OPTIONAL_TELEMETRY_ID);
|
|
expect(bridge).not.toMatch(
|
|
new RegExp(`^\\s*-\\s*${INTELLIGENCE_API_URL}=`, "m"),
|
|
);
|
|
expect(bridge).not.toMatch(
|
|
new RegExp(`^\\s*-\\s*${INTELLIGENCE_GATEWAY_WS_URL}=`, "m"),
|
|
);
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_API_KEY));
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(LEGACY_TELEMETRY_ID));
|
|
expect(contents).not.toContain(MANAGED_LICENSE_TOKEN);
|
|
}
|
|
|
|
/** Assert AgentCore deploy configuration carries only a secret reference. */
|
|
function expectAgentCoreDeploymentConfigContract(contents: string): void {
|
|
const configuredSecretNames = contents.split(/\r?\n/).flatMap((line) => {
|
|
const match = line.match(
|
|
new RegExp(
|
|
`^${MANAGED_API_KEY_SECRET_CONFIG}\\s*:\\s*([^#\\s][^#]*?)\\s*(?:#.*)?$`,
|
|
),
|
|
);
|
|
return match?.[1] ? [match[1].trim()] : [];
|
|
});
|
|
|
|
expect(configuredSecretNames).toHaveLength(1);
|
|
expect(configuredSecretNames[0]).not.toMatch(/^\$\{|^process\.env\b/);
|
|
expect(contents).not.toMatch(exactEnvIdentifierPattern(MANAGED_API_KEY));
|
|
expect(contents).not.toMatch(
|
|
exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN),
|
|
);
|
|
expect(contents).not.toContain("copilotkit_license_token_secret_name");
|
|
}
|
|
|
|
/** Assert AgentCore's deployed Lambda resolves the managed key from a secret. */
|
|
function expectAgentCoreRuntimeDeploymentContract(contents: string): void {
|
|
const sourceFile = parseManagedSource(contents);
|
|
const lambdaOptions = constructOptions(
|
|
sourceFile,
|
|
["lambda", "Function"],
|
|
"CopilotKitRuntimeLambda",
|
|
);
|
|
expect(lambdaOptions).toHaveLength(1);
|
|
const environment = lambdaOptions[0]
|
|
? objectPropertyAssignment(lambdaOptions[0], "environment")
|
|
: null;
|
|
const environmentObject = environment
|
|
? unwrapExpression(environment.initializer)
|
|
: null;
|
|
expect(
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject),
|
|
).toBe(true);
|
|
const managedKey =
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject)
|
|
? objectPropertyAssignment(environmentObject, MANAGED_API_KEY)
|
|
: null;
|
|
const telemetryId =
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject)
|
|
? objectPropertyAssignment(environmentObject, OPTIONAL_TELEMETRY_ID)
|
|
: null;
|
|
const licenseToken =
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject)
|
|
? objectPropertyAssignment(environmentObject, MANAGED_LICENSE_TOKEN)
|
|
: null;
|
|
const apiUrl =
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject)
|
|
? objectEnvironmentExpression(environmentObject, INTELLIGENCE_API_URL)
|
|
: null;
|
|
const gatewayWsUrl =
|
|
environmentObject && ts.isObjectLiteralExpression(environmentObject)
|
|
? objectEnvironmentExpression(
|
|
environmentObject,
|
|
INTELLIGENCE_GATEWAY_WS_URL,
|
|
)
|
|
: null;
|
|
expect(managedKey).not.toBeNull();
|
|
expect(telemetryId).not.toBeNull();
|
|
expect(licenseToken).toBeNull();
|
|
expect(apiUrl).not.toBeNull();
|
|
expect(gatewayWsUrl).not.toBeNull();
|
|
if (!managedKey || !telemetryId || !apiUrl || !gatewayWsUrl) return;
|
|
|
|
expect(
|
|
expressionContainsPropertyPath(managedKey.initializer, [
|
|
"config",
|
|
MANAGED_API_KEY_SECRET_CONFIG,
|
|
]),
|
|
).toBe(true);
|
|
expect(expressionContainsSecretResolution(managedKey.initializer)).toBe(true);
|
|
expect(
|
|
expressionContainsEnvRead(
|
|
managedKey.initializer,
|
|
MANAGED_API_KEY_SECRET_VERSION_ID,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
expressionContainsEnvRead(managedKey.initializer, MANAGED_API_KEY),
|
|
).toBe(false);
|
|
expect(
|
|
expressionContainsEnvRead(telemetryId.initializer, OPTIONAL_TELEMETRY_ID),
|
|
).toBe(true);
|
|
expect(expressionContainsSecretResolution(telemetryId.initializer)).toBe(
|
|
false,
|
|
);
|
|
expect(expressionContainsEnvRead(apiUrl, INTELLIGENCE_API_URL)).toBe(true);
|
|
expect(
|
|
expressionContainsEnvRead(gatewayWsUrl, INTELLIGENCE_GATEWAY_WS_URL),
|
|
).toBe(true);
|
|
}
|
|
|
|
/** Assert AgentCore does not carry a self-hosted license through managed AWS setup. */
|
|
function expectAgentCoreNoLicenseTokenContract(
|
|
deploymentConfig: string,
|
|
runtimeDeployment: string,
|
|
deployScripts: readonly string[],
|
|
): void {
|
|
for (const contents of [
|
|
deploymentConfig,
|
|
runtimeDeployment,
|
|
...deployScripts,
|
|
]) {
|
|
expect(contents).not.toMatch(
|
|
exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN),
|
|
);
|
|
expect(contents).not.toContain("copilotkit_license_token_secret_name");
|
|
expect(contents).not.toContain(
|
|
"COPILOTKIT_LICENSE_TOKEN_SECRET_VERSION_ID",
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Assert Terraform clearly excludes the managed Intelligence credential path. */
|
|
function expectAgentCoreTerraformExclusionContract(
|
|
runtimeDeployment: string,
|
|
readme: string,
|
|
): void {
|
|
expect(runtimeDeployment).not.toMatch(
|
|
exactEnvIdentifierPattern(MANAGED_API_KEY),
|
|
);
|
|
expect(runtimeDeployment).not.toMatch(
|
|
exactEnvIdentifierPattern(OPTIONAL_TELEMETRY_ID),
|
|
);
|
|
expect(readme).toMatch(/does not project managed Intelligence credentials/i);
|
|
expect(readme).toMatch(/use the CDK deployment path/i);
|
|
}
|
|
|
|
/** Assert an AgentCore variant deploy script safely materializes its key secret. */
|
|
function expectAgentCoreDeployScriptContract(contents: string): void {
|
|
const normalized = contents.replace(/\\\r?\n\s*/g, " ");
|
|
const lines = normalized.split(/\r?\n/);
|
|
const rootEnvLoadIndex = lines.findIndex((line) =>
|
|
/(?:^|\s)(?:source|\.)\s+["']?\$\{?SCRIPT_DIR\}?\/\.env["']?(?:\s|$)/.test(
|
|
line,
|
|
),
|
|
);
|
|
const secretConfigAssignment = lines
|
|
.map((line, index) => ({
|
|
index,
|
|
match: line.match(
|
|
new RegExp(`^\\s*([A-Z][A-Z0-9_]*)=.*${MANAGED_API_KEY_SECRET_CONFIG}`),
|
|
),
|
|
}))
|
|
.find(({ match }) => match?.[1]);
|
|
const secretNameVariable = secretConfigAssignment?.match?.[1];
|
|
const secretCommands = lines
|
|
.map((line, index) => ({ index, line }))
|
|
.filter(
|
|
({ line }) =>
|
|
line.includes(`$${MANAGED_API_KEY}`) &&
|
|
/aws\s+secretsmanager\s+(?:create-secret|put-secret-value)\b/.test(
|
|
line,
|
|
),
|
|
);
|
|
const cdkDeployIndex = lines.findIndex((line) =>
|
|
/npx\s+cdk(?:@\S+)?\s+deploy\b/.test(line),
|
|
);
|
|
const skipBackendGuardIndex = lines.findIndex((line) =>
|
|
/if\s+\[\s+"\$SKIP_BACKEND"\s+=\s+true\s+\];\s+then/.test(line),
|
|
);
|
|
const backendDeployElseIndex = lines.findIndex(
|
|
(line, index) => index > skipBackendGuardIndex && line.trim() === "else",
|
|
);
|
|
const backendDeployEndIndex = lines.findIndex(
|
|
(line, index) => index > cdkDeployIndex && line.trim() === "fi",
|
|
);
|
|
const telemetryExportIndex = lines.findIndex((line) =>
|
|
new RegExp(`^\\s*export\\s+${OPTIONAL_TELEMETRY_ID}(?:=|\\s|$)`).test(line),
|
|
);
|
|
const disableAllexportIndex = lines.findIndex((line) =>
|
|
/^\s*set\s+\+a\s*$/.test(line),
|
|
);
|
|
const disableXtraceIndex = lines.findIndex((line) =>
|
|
/^\s*set\s+\+x\s*$/.test(line),
|
|
);
|
|
const managedKeyUnexportIndices = lines.flatMap((line, index) =>
|
|
new RegExp(`^\\s*export\\s+-n\\s+${MANAGED_API_KEY}\\s*$`).test(line)
|
|
? [index]
|
|
: [],
|
|
);
|
|
const managedKeyUnsetIndices = lines.flatMap((line, index) =>
|
|
new RegExp(`^\\s*unset\\s+${MANAGED_API_KEY}\\s*$`).test(line)
|
|
? [index]
|
|
: [],
|
|
);
|
|
const secretVersionExportIndex = lines.findIndex((line) =>
|
|
new RegExp(
|
|
`^\\s*export\\s+${MANAGED_API_KEY_SECRET_VERSION_ID}(?:=|\\s|$)`,
|
|
).test(line),
|
|
);
|
|
|
|
expect(contents).not.toMatch(
|
|
exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN),
|
|
);
|
|
expect(contents).not.toContain("copilotkit_license_token_secret_name");
|
|
expect(contents).not.toContain("COPILOTKIT_LICENSE_TOKEN_SECRET_VERSION_ID");
|
|
|
|
expect(rootEnvLoadIndex).toBeGreaterThanOrEqual(0);
|
|
expect(secretNameVariable).toBeDefined();
|
|
expect(secretCommands.length).toBeGreaterThan(0);
|
|
if (
|
|
secretConfigAssignment === undefined ||
|
|
secretNameVariable === undefined ||
|
|
secretCommands[0] === undefined
|
|
) {
|
|
throw new Error("AgentCore secret materialization contract is incomplete");
|
|
}
|
|
expect(skipBackendGuardIndex).toBeGreaterThanOrEqual(0);
|
|
expect(disableAllexportIndex).toBeGreaterThanOrEqual(0);
|
|
expect(disableAllexportIndex).toBeLessThan(rootEnvLoadIndex);
|
|
expect(disableXtraceIndex).toBeGreaterThanOrEqual(0);
|
|
expect(disableXtraceIndex).toBeLessThan(rootEnvLoadIndex);
|
|
expect(contents).not.toMatch(/^\s*set\s+-a\s*$/m);
|
|
expect(
|
|
managedKeyUnexportIndices.some((index) => index < rootEnvLoadIndex),
|
|
).toBe(true);
|
|
expect(
|
|
managedKeyUnexportIndices.some(
|
|
(index) => index > rootEnvLoadIndex && index < skipBackendGuardIndex,
|
|
),
|
|
).toBe(true);
|
|
expect(backendDeployElseIndex).toBeGreaterThan(skipBackendGuardIndex);
|
|
expect(secretConfigAssignment.index).toBeGreaterThan(backendDeployElseIndex);
|
|
expect(secretVersionExportIndex).toBeGreaterThan(backendDeployElseIndex);
|
|
expect(backendDeployEndIndex).toBeGreaterThan(cdkDeployIndex);
|
|
expect(telemetryExportIndex).toBeGreaterThan(rootEnvLoadIndex);
|
|
expect(cdkDeployIndex).toBeGreaterThan(
|
|
Math.max(telemetryExportIndex, ...secretCommands.map(({ index }) => index)),
|
|
);
|
|
for (const { line } of secretCommands) {
|
|
expect(line).toMatch(
|
|
new RegExp(
|
|
`printf\\s+['"]%s['"]\\s+["']\\$\\{?${MANAGED_API_KEY}\\}?["']\\s*\\|\\s*aws\\s+secretsmanager`,
|
|
),
|
|
);
|
|
expect(line).toMatch(
|
|
/--secret-string(?:=|\s+)file:\/\/\/dev\/stdin(?:\s|$)/,
|
|
);
|
|
expect(line).not.toMatch(
|
|
new RegExp(
|
|
`--secret-string(?:=|\\s+)["']?\\$\\{?${MANAGED_API_KEY}\\}?["']?(?:\\s|$)`,
|
|
),
|
|
);
|
|
expect(line).toMatch(
|
|
new RegExp(
|
|
`--(?:name|secret-id)(?:=|\\s+)["']?\\$\\{?${secretNameVariable}\\}?["']?(?:\\s|$)`,
|
|
),
|
|
);
|
|
}
|
|
expect(rootEnvLoadIndex).toBeLessThan(secretCommands[0].index);
|
|
expect(secretConfigAssignment.index).toBeLessThan(secretCommands[0].index);
|
|
expect(
|
|
managedKeyUnsetIndices.some(
|
|
(index) =>
|
|
index > skipBackendGuardIndex && index < backendDeployElseIndex,
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
managedKeyUnsetIndices.some(
|
|
(index) => index > secretCommands[0].index && index < cdkDeployIndex,
|
|
),
|
|
).toBe(true);
|
|
for (const backendOwnedIndex of [
|
|
secretConfigAssignment.index,
|
|
...secretCommands.map(({ index: commandIndex }) => commandIndex),
|
|
secretVersionExportIndex,
|
|
cdkDeployIndex,
|
|
]) {
|
|
expect(backendOwnedIndex).toBeGreaterThan(backendDeployElseIndex);
|
|
expect(backendOwnedIndex).toBeLessThan(backendDeployEndIndex);
|
|
}
|
|
}
|
|
|
|
/** Returns exact path filters for one top-level workflow event. */
|
|
function workflowEventPaths(contents: string, eventName: string): string[] {
|
|
const lines = contents.split(/\r?\n/);
|
|
const eventStart = lines.findIndex((line) => line === ` ${eventName}:`);
|
|
if (eventStart < 0) return [];
|
|
const relativeEventEnd = lines
|
|
.slice(eventStart + 1)
|
|
.findIndex((line) => /^(?:\S| {2}\S).*:\s*$/.test(line));
|
|
const eventEnd =
|
|
relativeEventEnd < 0 ? lines.length : eventStart + relativeEventEnd + 1;
|
|
const eventLines = lines.slice(eventStart, eventEnd);
|
|
const pathsStart = eventLines.findIndex((line) => line === " paths:");
|
|
if (pathsStart < 0) return [];
|
|
|
|
const paths: string[] = [];
|
|
for (const line of eventLines.slice(pathsStart + 1)) {
|
|
const match = line.match(/^ - ["']?(.+?)["']?\s*$/);
|
|
if (!match?.[1]) break;
|
|
paths.push(match[1]);
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
interface WorkflowStep {
|
|
readonly name: string;
|
|
readonly run: string;
|
|
}
|
|
|
|
/** Returns named workflow steps with their complete run scripts. */
|
|
function workflowSteps(contents: string): WorkflowStep[] {
|
|
const lines = contents.split(/\r?\n/);
|
|
const stepStarts = lines.flatMap((line, index) =>
|
|
/^ - name:\s*/.test(line) ? [index] : [],
|
|
);
|
|
|
|
return stepStarts.map((start, position) => {
|
|
const end = stepStarts[position + 1] ?? lines.length;
|
|
const stepLines = lines.slice(start, end);
|
|
const name = stepLines[0]!
|
|
.replace(/^ - name:\s*/, "")
|
|
.replace(/^["']|["']$/g, "");
|
|
const runStart = stepLines.findIndex((line) =>
|
|
line.startsWith(" run:"),
|
|
);
|
|
const run =
|
|
runStart < 0
|
|
? ""
|
|
: stepLines
|
|
.slice(runStart)
|
|
.join("\n")
|
|
.replace(/^\s*run:\s*[|>-]?\s*/m, "")
|
|
.trim();
|
|
return { name, run };
|
|
});
|
|
}
|
|
|
|
/** Assert the parity workflow preserves filters and runs red contracts second. */
|
|
function expectIntegrationParityWorkflowContract(contents: string): void {
|
|
const expectedPaths = [
|
|
"examples/integrations/**",
|
|
"scripts/__tests__/integration-intelligence-migration.test.ts",
|
|
".github/workflows/integrations_parity.yml",
|
|
];
|
|
expect(workflowEventPaths(contents, "pull_request")).toEqual(expectedPaths);
|
|
expect(workflowEventPaths(contents, "push")).toEqual(expectedPaths);
|
|
|
|
const steps = workflowSteps(contents);
|
|
const parityIndex = steps.findIndex(
|
|
({ name }) => name === "Verify integration-demo parity",
|
|
);
|
|
const contractIndex = steps.findIndex(
|
|
({ name }) => name === "Verify Intelligence template credential contracts",
|
|
);
|
|
expect(parityIndex).toBeGreaterThanOrEqual(0);
|
|
expect(contractIndex).toBeGreaterThan(parityIndex);
|
|
expect(steps[parityIndex]?.run).toContain("pnpm parity:check");
|
|
expect(steps[contractIndex]?.run).toContain(
|
|
"pnpm exec vitest run scripts/__tests__/integration-intelligence-migration.test.ts",
|
|
);
|
|
}
|
|
|
|
/** Renders an exact workflow path-filter list for helper self-tests. */
|
|
function renderWorkflowPaths(values: readonly string[]): string {
|
|
return values.map((value) => ` - "${value}"`).join("\n");
|
|
}
|
|
|
|
for (const stack of INTELLIGENCE_DEV_STACKS) {
|
|
test(`${stack.integration} has a valid local Intelligence compose stack`, () => {
|
|
const compose = readIntelligenceDevSurface(
|
|
stack.integration,
|
|
"docker-compose.intelligence.yml",
|
|
);
|
|
const postgresInit = readIntelligenceDevSurface(
|
|
stack.integration,
|
|
"docker/postgres-init/01-create-databases.sql",
|
|
);
|
|
const readme = readIntelligenceDevSurface(stack.integration, "README.md");
|
|
const envExample = readOptionalIntelligenceDevSurface(
|
|
stack.integration,
|
|
".env.example",
|
|
);
|
|
|
|
expectDockerComposeConfigToSucceed(stack.integration);
|
|
expect(compose).toContain(`name: ${stack.projectName}`);
|
|
expect(compose).toContain(
|
|
`\${POSTGRES_HOST_PORT:-${stack.postgresPort}}:5432`,
|
|
);
|
|
expect(compose).toContain(`\${REDIS_HOST_PORT:-${stack.redisPort}}:6379`);
|
|
expect(compose).toContain(
|
|
`\${APP_API_HOST_PORT:-${stack.appApiPort}}:4201`,
|
|
);
|
|
expect(compose).toContain(
|
|
`\${GATEWAY_HOST_PORT:-${stack.gatewayPort}}:4401`,
|
|
);
|
|
expect(compose).toContain(
|
|
"./docker/postgres-init:/docker-entrypoint-initdb.d:ro",
|
|
);
|
|
expect(compose).not.toContain("./docker:/docker-entrypoint-initdb.d");
|
|
expect(compose).toContain("pg_isready -U intelligence -d intelligence_app");
|
|
expect(postgresInit).toBe(EXPECTED_POSTGRES_INIT_SQL);
|
|
expect(readme).toContain("## CopilotKit Intelligence & Threads (Optional)");
|
|
expect(readme).toContain("docker-compose.intelligence.yml");
|
|
expect(readme).toContain(
|
|
`INTELLIGENCE_API_URL=http://localhost:${stack.appApiPort}`,
|
|
);
|
|
expect(readme).toContain(
|
|
`INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:${stack.gatewayPort}`,
|
|
);
|
|
|
|
if (envExample) {
|
|
expect(envExample).toContain(
|
|
`INTELLIGENCE_API_URL=http://localhost:${stack.appApiPort}`,
|
|
);
|
|
expect(envExample).toContain(
|
|
`INTELLIGENCE_GATEWAY_WS_URL=ws://localhost:${stack.gatewayPort}`,
|
|
);
|
|
}
|
|
}, 30_000);
|
|
}
|
|
|
|
test("local Intelligence compose host defaults do not collide", () => {
|
|
const projectNames = new Set(
|
|
INTELLIGENCE_DEV_STACKS.map((stack) => stack.projectName),
|
|
);
|
|
const postgresPorts = new Set(
|
|
INTELLIGENCE_DEV_STACKS.map((stack) => stack.postgresPort),
|
|
);
|
|
const redisPorts = new Set(
|
|
INTELLIGENCE_DEV_STACKS.map((stack) => stack.redisPort),
|
|
);
|
|
const appApiPorts = new Set(
|
|
INTELLIGENCE_DEV_STACKS.map((stack) => stack.appApiPort),
|
|
);
|
|
const gatewayPorts = new Set(
|
|
INTELLIGENCE_DEV_STACKS.map((stack) => stack.gatewayPort),
|
|
);
|
|
|
|
expect(projectNames.size).toBe(INTELLIGENCE_DEV_STACKS.length);
|
|
expect(postgresPorts.size).toBe(INTELLIGENCE_DEV_STACKS.length);
|
|
expect(redisPorts.size).toBe(INTELLIGENCE_DEV_STACKS.length);
|
|
expect(appApiPorts.size).toBe(INTELLIGENCE_DEV_STACKS.length);
|
|
expect(gatewayPorts.size).toBe(INTELLIGENCE_DEV_STACKS.length);
|
|
});
|
|
|
|
test("local Intelligence compose files drift only by declared starter ports and project names", () => {
|
|
const referenceStack = INTELLIGENCE_DEV_STACKS[0];
|
|
const remainingStacks = INTELLIGENCE_DEV_STACKS.slice(1);
|
|
const referenceCompose = normalizeIntelligenceComposeForDriftGuard(
|
|
readIntelligenceDevSurface(
|
|
referenceStack.integration,
|
|
"docker-compose.intelligence.yml",
|
|
),
|
|
);
|
|
|
|
for (const stack of remainingStacks) {
|
|
const compose = normalizeIntelligenceComposeForDriftGuard(
|
|
readIntelligenceDevSurface(
|
|
stack.integration,
|
|
"docker-compose.intelligence.yml",
|
|
),
|
|
);
|
|
|
|
expect(compose).toBe(referenceCompose);
|
|
}
|
|
});
|
|
|
|
test("managed TypeScript helpers reject API-key identifiers outside configured expressions", () => {
|
|
const unusedRuntimeRead = `
|
|
const unused = process.env.CPK_INTELLIGENCE_API_KEY;
|
|
// apiKey: process.env.CPK_INTELLIGENCE_API_KEY
|
|
const intelligence = { apiKey: "CPK_INTELLIGENCE_API_KEY" };
|
|
`;
|
|
const unusedGateRead = `
|
|
const unused = process.env["CPK_INTELLIGENCE_API_KEY"];
|
|
// NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: process.env.CPK_INTELLIGENCE_API_KEY
|
|
export default {
|
|
env: { NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: "false" },
|
|
};
|
|
`;
|
|
|
|
expect(() => expectManagedRuntimeContract(unusedRuntimeRead)).toThrow();
|
|
expect(() => expectManagedGateContract(unusedGateRead)).toThrow();
|
|
});
|
|
|
|
test("managed TypeScript helpers reject decoy configured properties", () => {
|
|
const decoyRuntimeRead = `
|
|
const decoy = { apiKey: process.env.CPK_INTELLIGENCE_API_KEY };
|
|
const intelligence = new CopilotKitIntelligence({ apiKey: "" });
|
|
`;
|
|
const decoyNextGateRead = `
|
|
const decoy = {
|
|
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED:
|
|
process.env.CPK_INTELLIGENCE_API_KEY ? "true" : "false",
|
|
};
|
|
const nextConfig = {
|
|
env: { NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED: "false" },
|
|
};
|
|
export default nextConfig;
|
|
`;
|
|
expect(() => expectManagedRuntimeContract(decoyRuntimeRead)).toThrow();
|
|
expect(() => expectManagedGateContract(decoyNextGateRead)).toThrow();
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
name: "inline client",
|
|
source: `
|
|
const runtime = new CopilotRuntime({
|
|
agents: {},
|
|
intelligence: new CopilotKitIntelligence({
|
|
apiKey: process.env.CPK_INTELLIGENCE_API_KEY,
|
|
}),
|
|
});
|
|
`,
|
|
},
|
|
{
|
|
name: "bound client",
|
|
source: `
|
|
const intelligence = new CopilotKitIntelligence({
|
|
apiKey: process.env.CPK_INTELLIGENCE_API_KEY,
|
|
});
|
|
const runtime = new CopilotRuntime({ agents: {}, intelligence });
|
|
`,
|
|
},
|
|
])(
|
|
"managed Runtime helper accepts a $name wired into the Runtime",
|
|
({ source }) => {
|
|
expect(() => expectManagedRuntimeContract(source)).not.toThrow();
|
|
},
|
|
);
|
|
|
|
test("managed Runtime helper rejects a configured Intelligence client that is not wired into the Runtime", () => {
|
|
const unusedConfiguredClient = `
|
|
const intelligence = new CopilotKitIntelligence({
|
|
apiKey: process.env.CPK_INTELLIGENCE_API_KEY,
|
|
});
|
|
const runtime = new CopilotRuntime({
|
|
agents: { default: new HttpAgent({ url: "http://localhost:8000" }) },
|
|
});
|
|
`;
|
|
|
|
expect(() => expectManagedRuntimeContract(unusedConfiguredClient)).toThrow();
|
|
});
|
|
|
|
test("managed gate helpers reject direct, concatenated, comment-only, and decoy key exposure", () => {
|
|
const invalidGateSources = [
|
|
`export default { env: { ${NEXT_THREADS_GATE}: process.env.${MANAGED_API_KEY} } };`,
|
|
`export default { env: { ${NEXT_THREADS_GATE}: "enabled-" + process.env.${MANAGED_API_KEY} } };`,
|
|
`
|
|
// ${NEXT_THREADS_GATE}: process.env.${MANAGED_API_KEY} ? "true" : "false"
|
|
export default { env: { ${NEXT_THREADS_GATE}: "false" } };
|
|
`,
|
|
`
|
|
const decoy = process.env.${MANAGED_API_KEY} ? "true" : "false";
|
|
export default { env: { ${NEXT_THREADS_GATE}: "false" } };
|
|
`,
|
|
];
|
|
|
|
for (const source of invalidGateSources) {
|
|
expect(() => expectManagedGateContract(source)).toThrow();
|
|
}
|
|
});
|
|
|
|
test("managed gate helpers accept a normalized Next boolean projection", () => {
|
|
const nextGate = `
|
|
export default {
|
|
env: {
|
|
${NEXT_THREADS_GATE}: process.env.${MANAGED_API_KEY}
|
|
? "true"
|
|
: "false",
|
|
},
|
|
};
|
|
`;
|
|
|
|
expect(() => expectManagedGateContract(nextGate)).not.toThrow();
|
|
});
|
|
|
|
test("AgentCore deployment does not forward the unused client entitlement marker", () => {
|
|
const contract = INTELLIGENCE_TEMPLATE_CONTRACTS.find(
|
|
({ directory }) => directory === "agentcore",
|
|
);
|
|
if (!contract || !("supportedPaths" in contract)) {
|
|
throw new Error("AgentCore managed template contract is missing");
|
|
}
|
|
|
|
const frontendDeployment = readManagedSurface(
|
|
contract,
|
|
contract.supportedPaths.frontendDeploymentPath,
|
|
"frontend deployment config",
|
|
);
|
|
const deployScripts = contract.supportedPaths.deployScriptPaths.map(
|
|
(deployScriptPath) =>
|
|
readManagedSurface(contract, deployScriptPath, "deploy script"),
|
|
);
|
|
|
|
expect(frontendDeployment).not.toMatch(
|
|
exactEnvIdentifierPattern(VITE_THREADS_GATE),
|
|
);
|
|
for (const deployScript of deployScripts) {
|
|
expect(deployScript).not.toMatch(
|
|
exactEnvIdentifierPattern(VITE_THREADS_GATE),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("AgentCore structural helpers accept linked root-env, secret, and Lambda wiring", () => {
|
|
const deploymentConfig = `${MANAGED_API_KEY_SECRET_CONFIG}: cpk-managed-key`;
|
|
const runtimeDeployment = `
|
|
new lambda.Function(this, "CopilotKitRuntimeLambda", {
|
|
environment: {
|
|
${MANAGED_API_KEY}: cdk.SecretValue.secretsManager(
|
|
config.${MANAGED_API_KEY_SECRET_CONFIG},
|
|
{ versionId: process.env.${MANAGED_API_KEY_SECRET_VERSION_ID} },
|
|
).unsafeUnwrap(),
|
|
${OPTIONAL_TELEMETRY_ID}: process.env.${OPTIONAL_TELEMETRY_ID} ?? "",
|
|
${INTELLIGENCE_API_URL}: process.env.${INTELLIGENCE_API_URL} ?? "http://localhost:4201",
|
|
${INTELLIGENCE_GATEWAY_WS_URL}: process.env.${INTELLIGENCE_GATEWAY_WS_URL} ?? "ws://localhost:4401",
|
|
},
|
|
});
|
|
`;
|
|
const deployScript = `
|
|
set +a
|
|
set +x
|
|
export -n ${MANAGED_API_KEY}
|
|
source "$SCRIPT_DIR/.env"
|
|
export -n ${MANAGED_API_KEY}
|
|
export ${OPTIONAL_TELEMETRY_ID}
|
|
if [ "$SKIP_BACKEND" = true ]; then
|
|
unset ${MANAGED_API_KEY}
|
|
echo "Skipping backend"
|
|
else
|
|
CPK_SECRET_NAME=$(read_config ${MANAGED_API_KEY_SECRET_CONFIG})
|
|
printf '%s' "$${MANAGED_API_KEY}" | aws secretsmanager create-secret --name "$CPK_SECRET_NAME" --secret-string file:///dev/stdin
|
|
unset ${MANAGED_API_KEY}
|
|
export ${MANAGED_API_KEY_SECRET_VERSION_ID}
|
|
npx cdk deploy --all
|
|
fi
|
|
`;
|
|
|
|
expect(() =>
|
|
expectAgentCoreDeploymentConfigContract(deploymentConfig),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
expectAgentCoreRuntimeDeploymentContract(runtimeDeployment),
|
|
).not.toThrow();
|
|
expect(() => expectAgentCoreDeployScriptContract(deployScript)).not.toThrow();
|
|
});
|
|
|
|
test("AgentCore structural helpers reject comment, unrelated-secret, and disconnected-command decoys", () => {
|
|
const runtimeDecoy = `
|
|
// ${MANAGED_API_KEY} uses config.${MANAGED_API_KEY_SECRET_CONFIG}
|
|
const unrelated = cdk.SecretValue.secretsManager("other-secret");
|
|
new lambda.Function(this, "CopilotKitRuntimeLambda", {
|
|
environment: {
|
|
${MANAGED_API_KEY}: "literal-key",
|
|
${OPTIONAL_TELEMETRY_ID}: "literal-telemetry-id",
|
|
},
|
|
});
|
|
`;
|
|
const runtimeTelemetrySecretDecoy = `
|
|
new lambda.Function(this, "CopilotKitRuntimeLambda", {
|
|
environment: {
|
|
${MANAGED_API_KEY}: cdk.SecretValue.secretsManager(
|
|
config.${MANAGED_API_KEY_SECRET_CONFIG},
|
|
).unsafeUnwrap(),
|
|
${OPTIONAL_TELEMETRY_ID}: cdk.SecretValue.secretsManager(
|
|
config.${MANAGED_API_KEY_SECRET_CONFIG},
|
|
).unsafeUnwrap(),
|
|
},
|
|
});
|
|
`;
|
|
const deployScriptDecoy = `
|
|
# source "$SCRIPT_DIR/.env"
|
|
CPK_SECRET_NAME=$(read_config ${MANAGED_API_KEY_SECRET_CONFIG})
|
|
echo "$${MANAGED_API_KEY}"
|
|
aws secretsmanager create-secret --name unrelated --secret-string literal
|
|
npx cdk deploy --all
|
|
export ${OPTIONAL_TELEMETRY_ID}
|
|
`;
|
|
const lateTelemetryExportDecoy = `
|
|
source "$SCRIPT_DIR/.env"
|
|
CPK_SECRET_NAME=$(read_config ${MANAGED_API_KEY_SECRET_CONFIG})
|
|
aws secretsmanager create-secret --name "$CPK_SECRET_NAME" --secret-string "$${MANAGED_API_KEY}"
|
|
npx cdk deploy --all
|
|
export ${OPTIONAL_TELEMETRY_ID}
|
|
`;
|
|
|
|
expect(() =>
|
|
expectAgentCoreRuntimeDeploymentContract(runtimeDecoy),
|
|
).toThrow();
|
|
expect(() =>
|
|
expectAgentCoreRuntimeDeploymentContract(runtimeTelemetrySecretDecoy),
|
|
).toThrow();
|
|
expect(() =>
|
|
expectAgentCoreDeployScriptContract(deployScriptDecoy),
|
|
).toThrow();
|
|
expect(() =>
|
|
expectAgentCoreDeployScriptContract(lateTelemetryExportDecoy),
|
|
).toThrow();
|
|
expect(() =>
|
|
expectAgentCoreDeploymentConfigContract(
|
|
`${MANAGED_API_KEY_SECRET_CONFIG}: \${${MANAGED_API_KEY}}`,
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
test("AgentCore deployment helper rejects endpoint literals disconnected from the root env", () => {
|
|
const runtimeDeployment = `
|
|
new lambda.Function(this, "CopilotKitRuntimeLambda", {
|
|
environment: {
|
|
${MANAGED_API_KEY}: cdk.SecretValue.secretsManager(
|
|
config.${MANAGED_API_KEY_SECRET_CONFIG},
|
|
).unsafeUnwrap(),
|
|
${OPTIONAL_TELEMETRY_ID}: process.env.${OPTIONAL_TELEMETRY_ID} ?? "",
|
|
${INTELLIGENCE_API_URL}: "https://hard-coded.example",
|
|
${INTELLIGENCE_GATEWAY_WS_URL}: "wss://hard-coded.example",
|
|
},
|
|
});
|
|
`;
|
|
|
|
expect(() =>
|
|
expectAgentCoreRuntimeDeploymentContract(runtimeDeployment),
|
|
).toThrow();
|
|
});
|
|
|
|
test("AgentCore README documents caller-overridden remote endpoints for both AWS deploy variants", () => {
|
|
const readme = fs.readFileSync(
|
|
path.join(integrationsDir, "agentcore", "README.md"),
|
|
"utf8",
|
|
);
|
|
|
|
expectAgentCoreAwsEndpointReadmeContract(readme);
|
|
});
|
|
|
|
for (const scriptName of [
|
|
"deploy-langgraph.sh",
|
|
"deploy-strands.sh",
|
|
] as const) {
|
|
test(`${scriptName} validates AWS endpoint overrides before secret or CDK commands`, () => {
|
|
const deployScript = fs.readFileSync(
|
|
path.join(integrationsDir, "agentcore", scriptName),
|
|
"utf8",
|
|
);
|
|
|
|
expectAgentCoreAwsEndpointDeployContract(deployScript);
|
|
});
|
|
|
|
for (const invalidEndpoint of [
|
|
{
|
|
label: "localhost API URL",
|
|
envFile: {
|
|
apiUrl: "http://localhost:4201",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
expectedVariable: INTELLIGENCE_API_URL,
|
|
},
|
|
{
|
|
label: "127.0.0.1 gateway URL",
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "ws://127.0.0.1:4401",
|
|
},
|
|
expectedVariable: INTELLIGENCE_GATEWAY_WS_URL,
|
|
},
|
|
{
|
|
label: "Docker host API URL",
|
|
envFile: {
|
|
apiUrl: "http://host.docker.internal:4201",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
expectedVariable: INTELLIGENCE_API_URL,
|
|
expectedMessage: /reachable from AWS/i,
|
|
},
|
|
{
|
|
label: "non-TLS remote API URL",
|
|
envFile: {
|
|
apiUrl: "http://intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
expectedVariable: INTELLIGENCE_API_URL,
|
|
expectedMessage: /https:\/\//i,
|
|
},
|
|
{
|
|
label: "non-TLS remote gateway URL",
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "ws://gateway.example.com/runtime",
|
|
},
|
|
expectedVariable: INTELLIGENCE_GATEWAY_WS_URL,
|
|
expectedMessage: /wss:\/\//i,
|
|
},
|
|
] as const) {
|
|
test(`${scriptName} rejects ${invalidEndpoint.label} without exposing the managed key`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: invalidEndpoint.envFile,
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.output).toContain(invalidEndpoint.expectedVariable);
|
|
expect(result.output).toMatch(
|
|
"expectedMessage" in invalidEndpoint
|
|
? invalidEndpoint.expectedMessage
|
|
: /reachable from AWS/i,
|
|
);
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
expect(result.cdkEnvironment).toBeNull();
|
|
});
|
|
}
|
|
|
|
test(`${scriptName} uses managed endpoint defaults when overrides are omitted`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {},
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.cdkEnvironment).toBe("\n\n");
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
});
|
|
|
|
test(`${scriptName} accepts sourced HTTPS and WSS endpoints and passes them to CDK`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.cdkEnvironment).toBe(
|
|
"https://intelligence.example.com/api\n" +
|
|
"wss://gateway.example.com/runtime\n",
|
|
);
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
});
|
|
|
|
test(`${scriptName} passes the updated secret version to CDK without logging it`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.secretVersionId).toBe(
|
|
MANAGED_API_KEY_SECRET_VERSION_SENTINEL,
|
|
);
|
|
expect(result.output).not.toContain(
|
|
MANAGED_API_KEY_SECRET_VERSION_SENTINEL,
|
|
);
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
});
|
|
|
|
test(`${scriptName} delivers the managed key only through Secrets Manager stdin`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
skipFrontend: false,
|
|
xtrace: true,
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.secretInputObservations).toEqual(["aws:key-stdin-match"]);
|
|
expect(result.secretExposureObservations).toEqual([]);
|
|
expect(result.awsCommands.join("\n")).not.toContain(
|
|
MANAGED_API_KEY_SENTINEL,
|
|
);
|
|
});
|
|
|
|
test(`${scriptName} --skip-backend leaves backend secret state untouched while deploying the frontend`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {
|
|
apiUrl: "https://intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://gateway.example.com/runtime",
|
|
},
|
|
skipBackend: true,
|
|
skipFrontend: false,
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.awsCommands).toEqual([
|
|
"sts get-caller-identity --query Account --output text",
|
|
]);
|
|
expect(result.npmCommands).toEqual([]);
|
|
expect(result.cdkEnvironment).toBeNull();
|
|
expect(result.secretVersionId).toBeNull();
|
|
expect(result.frontendStackName).toBe(
|
|
scriptName === "deploy-langgraph.sh"
|
|
? "agentcore-contract-lg"
|
|
: "agentcore-contract-st",
|
|
);
|
|
expect(result.frontendSecretVersionId).toBe("");
|
|
expect(result.secretExposureObservations).toEqual([]);
|
|
expect(result.secretInputObservations).toEqual([]);
|
|
expect(result.output).toContain("Skipping backend deploy (--skip-backend)");
|
|
expect(result.output).not.toContain("Managed Intelligence key stored");
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
expect(result.output).not.toContain(
|
|
MANAGED_API_KEY_SECRET_VERSION_SENTINEL,
|
|
);
|
|
});
|
|
|
|
test(`${scriptName} preserves documented caller endpoint overrides over root localhost defaults`, () => {
|
|
const result = runAgentCoreDeployHarness({
|
|
scriptName,
|
|
envFile: {
|
|
apiUrl: "http://localhost:4201",
|
|
gatewayWsUrl: "ws://localhost:4401",
|
|
},
|
|
callerEnvironment: {
|
|
apiUrl: "https://managed-intelligence.example.com/api",
|
|
gatewayWsUrl: "wss://managed-gateway.example.com/runtime",
|
|
},
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.cdkEnvironment).toBe(
|
|
"https://managed-intelligence.example.com/api\n" +
|
|
"wss://managed-gateway.example.com/runtime\n",
|
|
);
|
|
expect(result.output).not.toContain(MANAGED_API_KEY_SENTINEL);
|
|
});
|
|
}
|
|
|
|
test("managed TypeScript helpers reject malformed source", () => {
|
|
const malformedRuntime = `
|
|
const intelligence = new CopilotKitIntelligence({
|
|
apiKey: process.env.CPK_INTELLIGENCE_API_KEY,
|
|
});
|
|
}`;
|
|
const malformedGate = `
|
|
export default {
|
|
env: {
|
|
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED:
|
|
process.env.CPK_INTELLIGENCE_API_KEY ? "true" : "false",
|
|
},
|
|
`;
|
|
|
|
expect(() => expectManagedRuntimeContract(malformedRuntime)).toThrow();
|
|
expect(() => expectManagedGateContract(malformedGate)).toThrow();
|
|
});
|
|
|
|
test("managed TypeScript helpers accept independent telemetry reads", () => {
|
|
const configuredRuntimeRead = `
|
|
const telemetryId = process.env.CPK_TELEMETRY_ID;
|
|
const intelligence = new CopilotKitIntelligence({
|
|
apiKey: process.env["CPK_INTELLIGENCE_API_KEY"] ?? "",
|
|
});
|
|
const runtime = new CopilotRuntime({ agents: {}, intelligence });
|
|
`;
|
|
const configuredGateRead = `
|
|
const telemetryId = process.env["CPK_TELEMETRY_ID"];
|
|
const nextConfig = {
|
|
env: {
|
|
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED:
|
|
process.env.CPK_INTELLIGENCE_API_KEY ? "true" : "false",
|
|
},
|
|
};
|
|
export default nextConfig;
|
|
`;
|
|
expect(() =>
|
|
expectManagedRuntimeContract(configuredRuntimeRead),
|
|
).not.toThrow();
|
|
expect(() => expectManagedGateContract(configuredGateRead)).not.toThrow();
|
|
});
|
|
|
|
test("managed TypeScript helpers reject telemetry prerequisites", () => {
|
|
const telemetryGatedRuntime = `
|
|
const intelligence = new CopilotKitIntelligence({
|
|
apiKey:
|
|
process.env.CPK_INTELLIGENCE_API_KEY &&
|
|
process.env.CPK_TELEMETRY_ID,
|
|
});
|
|
const runtime = new CopilotRuntime({ agents: {}, intelligence });
|
|
`;
|
|
const telemetryGatedNextConfig = `
|
|
const nextConfig = {
|
|
env: {
|
|
NEXT_PUBLIC_COPILOTKIT_THREADS_ENABLED:
|
|
process.env.CPK_INTELLIGENCE_API_KEY &&
|
|
process.env.CPK_TELEMETRY_ID
|
|
? "true"
|
|
: "false",
|
|
},
|
|
};
|
|
export default nextConfig;
|
|
`;
|
|
expect(() => expectManagedRuntimeContract(telemetryGatedRuntime)).toThrow();
|
|
expect(() => expectManagedGateContract(telemetryGatedNextConfig)).toThrow();
|
|
});
|
|
|
|
/** Build the exact managed dotenv sections with telemetry semantics documented. */
|
|
function managedEnvExample(
|
|
telemetryDescription = "Optional, non-secret analytics identity.",
|
|
): string {
|
|
return [
|
|
"# Your CopilotKit Intelligence API Key",
|
|
"# Project Name: xxxxxxxxxx",
|
|
"CPK_INTELLIGENCE_API_KEY=xxxxx",
|
|
"",
|
|
"# CopilotKit Telemetry ID",
|
|
`# ${telemetryDescription}`,
|
|
"CPK_TELEMETRY_ID=xxxxx",
|
|
].join("\n");
|
|
}
|
|
|
|
test("managed documentation helpers reject misleading telemetry semantics", () => {
|
|
const misleadingDescriptions = [
|
|
"authentication credential",
|
|
"entitlement identity",
|
|
"feature enablement identity",
|
|
"analytics consent identity",
|
|
"telemetry send-policy identity",
|
|
];
|
|
|
|
for (const description of misleadingDescriptions) {
|
|
const envExample = managedEnvExample(
|
|
`Optional, non-secret analytics ${description}.`,
|
|
);
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set `CPK_INTELLIGENCE_API_KEY` for the project.",
|
|
"",
|
|
`\`CPK_TELEMETRY_ID\` is an optional, non-secret analytics ${description}.`,
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
}
|
|
});
|
|
|
|
test("managed documentation helpers accept an optional non-secret analytics identity", () => {
|
|
const envExample = managedEnvExample();
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set `CPK_INTELLIGENCE_API_KEY` for the project.",
|
|
"",
|
|
"`CPK_TELEMETRY_ID` is an optional, non-secret analytics identity.",
|
|
"",
|
|
"## Offline or self-hosted licensing",
|
|
"",
|
|
"Set `COPILOTKIT_LICENSE_TOKEN` only for an offline deployment.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).not.toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).not.toThrow();
|
|
});
|
|
|
|
test("managed SDK compatibility rejects unclassified package versions", () => {
|
|
const pin = {
|
|
packageName: "@copilotkit/runtime",
|
|
packagePath: "package.json",
|
|
version: "9.9.9",
|
|
} as const satisfies ManagedSdkPin;
|
|
|
|
expect(() => pinSupportsManagedEntitlements(pin)).toThrow(
|
|
/must be classified/,
|
|
);
|
|
});
|
|
|
|
test("managed README helper rejects missing offline or self-hosted license guidance", () => {
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set `CPK_INTELLIGENCE_API_KEY` for the project.",
|
|
"",
|
|
"`CPK_TELEMETRY_ID` is an optional, non-secret analytics identity.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
invalidLayout: "two blank lines between sections",
|
|
envExample: managedEnvExample().replace(
|
|
"CPK_INTELLIGENCE_API_KEY=xxxxx\n\n# CopilotKit",
|
|
"CPK_INTELLIGENCE_API_KEY=xxxxx\n\n\n# CopilotKit",
|
|
),
|
|
},
|
|
{
|
|
invalidLayout: "duplicate telemetry section",
|
|
envExample: `${managedEnvExample()}\n\n# CopilotKit Telemetry ID\nCPK_TELEMETRY_ID=duplicate`,
|
|
},
|
|
])("managed documentation helpers reject $invalidLayout", ({ envExample }) => {
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers reject license guidance inside the telemetry section", () => {
|
|
const envExample = `${managedEnvExample()}\nCOPILOTKIT_LICENSE_TOKEN=`;
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
"COPILOTKIT_LICENSE_TOKEN is not a managed credential.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers reject separated unlabeled license guidance", () => {
|
|
const envExample = [
|
|
managedEnvExample(),
|
|
"",
|
|
"# Self-hosted / offline license",
|
|
"COPILOTKIT_LICENSE_TOKEN=",
|
|
"",
|
|
"# Troubleshooting",
|
|
"COPILOTKIT_LICENSE_TOKEN=",
|
|
].join("\n");
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
"",
|
|
"## Self-hosted / offline license",
|
|
"",
|
|
"Offline deployments may set COPILOTKIT_LICENSE_TOKEN.",
|
|
"",
|
|
"## Troubleshooting",
|
|
"",
|
|
"Set COPILOTKIT_LICENSE_TOKEN if the managed integration is unavailable.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers reject reordered managed dotenv sections", () => {
|
|
const envExample = [
|
|
"# CopilotKit Telemetry ID",
|
|
"# Optional, non-secret analytics identity.",
|
|
"CPK_TELEMETRY_ID=xxxxx",
|
|
"",
|
|
"# Your CopilotKit Intelligence API Key",
|
|
"# Project Name: xxxxxxxxxx",
|
|
"CPK_INTELLIGENCE_API_KEY=xxxxx",
|
|
].join("\n");
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"",
|
|
"## Analytics identity",
|
|
"",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers allow separate self-hosted and offline license guidance", () => {
|
|
const envExample = [
|
|
managedEnvExample(),
|
|
"",
|
|
"# Self-hosted / offline license",
|
|
"COPILOTKIT_LICENSE_TOKEN=",
|
|
].join("\n");
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
"",
|
|
"## Self-hosted / offline license",
|
|
"",
|
|
"Offline deployments may set COPILOTKIT_LICENSE_TOKEN instead.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).not.toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).not.toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers reject unlabeled generic license-gating copy without an env literal", () => {
|
|
const envExample = [
|
|
managedEnvExample(),
|
|
"",
|
|
"# Product capabilities",
|
|
"# A license unlocks Threads and enables Inspector.",
|
|
].join("\n");
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
"",
|
|
"## Product capabilities",
|
|
"",
|
|
"A license activates Threads and unlocks Inspector.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).toThrow();
|
|
});
|
|
|
|
test("managed documentation helpers allow generic license-gating copy only in self-hosted or offline sections", () => {
|
|
const envExample = [
|
|
managedEnvExample(),
|
|
"",
|
|
"# Self-hosted / offline license behavior",
|
|
"# A deployment license enables Threads and Inspector offline.",
|
|
].join("\n");
|
|
const readme = [
|
|
"## Managed Intelligence credentials",
|
|
"",
|
|
"Set CPK_INTELLIGENCE_API_KEY for the project.",
|
|
"CPK_TELEMETRY_ID is an optional, non-secret analytics identity.",
|
|
"",
|
|
"## Self-hosted / offline license behavior",
|
|
"",
|
|
"Set COPILOTKIT_LICENSE_TOKEN only for this deployment mode.",
|
|
"A deployment license enables Threads and Inspector offline.",
|
|
].join("\n");
|
|
|
|
expect(() => expectManagedEnvContract(envExample)).not.toThrow();
|
|
expect(() => expectManagedReadmeContract(readme)).not.toThrow();
|
|
});
|
|
|
|
test("route preservation helper rejects an incomplete handler export set", () => {
|
|
const route = `
|
|
const app = createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" });
|
|
export const GET = handle(app);
|
|
export const POST = handle(app);
|
|
export const PATCH = handle(app);
|
|
`;
|
|
|
|
expect(() => expectEndpointHandlerContract(route)).toThrow();
|
|
});
|
|
|
|
test("frontend preservation helper rejects controlled thread state and single-endpoint transport", () => {
|
|
const frontend = `
|
|
export function App() {
|
|
return (
|
|
<CopilotKit runtimeUrl="/api/copilotkit" useSingleEndpoint={true}>
|
|
<CopilotChatConfigurationProvider agentId="default" threadId="thread-1">
|
|
<CopilotThreadsDrawer agentId="default" />
|
|
<CopilotChat />
|
|
</CopilotChatConfigurationProvider>
|
|
</CopilotKit>
|
|
);
|
|
}
|
|
`;
|
|
|
|
expect(() => expectFrontendThreadContract(frontend, frontend)).toThrow();
|
|
});
|
|
|
|
test("MCP preservation helper rejects a middleware-only scaffold without server behavior", () => {
|
|
const server = `
|
|
export function createServer() {
|
|
return new McpServer({ name: "Three.js Server", version: "1.0.0" });
|
|
}
|
|
`;
|
|
const transport = `
|
|
export function startServer() {
|
|
const app = createMcpExpressApp();
|
|
app.all("/mcp", () => undefined);
|
|
}
|
|
`;
|
|
|
|
expect(() => expectMcpServerBehavior(server, transport)).toThrow();
|
|
});
|
|
|
|
test("A2A preservation helper rejects collapsed single-agent routing", () => {
|
|
const route = `
|
|
const agent = new RuntimeA2AMiddlewareAgent({
|
|
agentId: "a2a_chat",
|
|
agentUrls: [],
|
|
orchestrationAgentUrl: "http://localhost:9000",
|
|
});
|
|
`;
|
|
|
|
expect(() => expectA2ARuntimeBehavior(route)).toThrow();
|
|
});
|
|
|
|
test("AgentCore preservation helper rejects disconnected local services", () => {
|
|
const compose = `
|
|
services:
|
|
agent:
|
|
image: agent
|
|
bridge:
|
|
image: bridge
|
|
frontend:
|
|
image: frontend
|
|
`;
|
|
|
|
expect(() => expectAgentCoreNetworkingBehavior(compose)).toThrow();
|
|
});
|
|
|
|
test("AgentCore preservation helper rejects a custom runner disconnected from Runtime selection", () => {
|
|
const runtime = `
|
|
class AgentCoreRunner extends InMemoryAgentRunner {}
|
|
const disconnectedRunner = new AgentCoreRunner();
|
|
const runtime = process.env.${MANAGED_API_KEY}
|
|
? new CopilotRuntime({
|
|
agents: {},
|
|
intelligence: new CopilotKitIntelligence({
|
|
apiKey: process.env.${MANAGED_API_KEY},
|
|
}),
|
|
identifyUser: () => ({ id: "demo-user" }),
|
|
})
|
|
: new CopilotRuntime({
|
|
agents: {},
|
|
runner: new InMemoryAgentRunner(),
|
|
});
|
|
const app = createCopilotEndpoint({ runtime, basePath: "/copilotkit" });
|
|
`;
|
|
|
|
expect(() => expectAgentCoreRuntimeBehavior(runtime)).toThrow();
|
|
});
|
|
|
|
test("integration parity workflow preserves filters and runs parity before the expected-red managed contract", () => {
|
|
const workflow = fs.readFileSync(INTEGRATION_PARITY_WORKFLOW, "utf8");
|
|
|
|
expectIntegrationParityWorkflowContract(workflow);
|
|
});
|
|
|
|
test("integration parity workflow helper rejects missing filters and reversed contract order", () => {
|
|
const paths = [
|
|
"examples/integrations/**",
|
|
"scripts/__tests__/integration-intelligence-migration.test.ts",
|
|
".github/workflows/integrations_parity.yml",
|
|
];
|
|
const workflow = (eventPaths: readonly string[], reversed: boolean) => `
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
${renderWorkflowPaths(eventPaths)}
|
|
push:
|
|
paths:
|
|
${renderWorkflowPaths(paths)}
|
|
jobs:
|
|
parity-check:
|
|
steps:
|
|
- name: ${reversed ? "Verify Intelligence template credential contracts" : "Verify integration-demo parity"}
|
|
run: ${reversed ? "pnpm exec vitest run scripts/__tests__/integration-intelligence-migration.test.ts" : "pnpm parity:check"}
|
|
- name: ${reversed ? "Verify integration-demo parity" : "Verify Intelligence template credential contracts"}
|
|
run: ${reversed ? "pnpm parity:check" : "pnpm exec vitest run scripts/__tests__/integration-intelligence-migration.test.ts"}
|
|
`;
|
|
|
|
expect(() =>
|
|
expectIntegrationParityWorkflowContract(workflow(paths, true)),
|
|
).toThrow();
|
|
expect(() =>
|
|
expectIntegrationParityWorkflowContract(workflow(paths.slice(0, 2), false)),
|
|
).toThrow();
|
|
});
|
|
|
|
test("the 17 Intelligence template directories back all 19 in-repo CLI frameworks", () => {
|
|
const frameworks = INTELLIGENCE_TEMPLATE_CONTRACTS.flatMap(
|
|
(contract) => contract.frameworks,
|
|
);
|
|
const ordinaryRuntimeContracts = INTELLIGENCE_TEMPLATE_CONTRACTS.filter(
|
|
(contract) => "runtimeAgent" in contract,
|
|
);
|
|
|
|
expect(INTELLIGENCE_TEMPLATE_CONTRACTS).toHaveLength(17);
|
|
expect(ordinaryRuntimeContracts).toHaveLength(14);
|
|
expect(new Set(frameworks).size).toBe(19);
|
|
expect([...frameworks].sort()).toEqual(
|
|
[...INTELLIGENCE_CLI_FRAMEWORKS].sort(),
|
|
);
|
|
});
|
|
|
|
test("agentcore managed setup does not document a self-hosted license token", () => {
|
|
const contract = INTELLIGENCE_TEMPLATE_CONTRACTS.find(
|
|
({ directory }) => directory === "agentcore",
|
|
);
|
|
|
|
expect(contract).not.toBeUndefined();
|
|
if (!contract) return;
|
|
|
|
const readme = readManagedSurface(contract, contract.readmePath, "README");
|
|
expect(readme).not.toMatch(exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN));
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
defect: "registered constructor changes",
|
|
contents: `
|
|
const defaultAgent = new WrongAgent({
|
|
url: process.env.AGENT_URL || "http://localhost:8000/",
|
|
});
|
|
new CopilotRuntime({ agents: { default: defaultAgent } });
|
|
`,
|
|
contract: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
defect: "required endpoint configuration disappears",
|
|
contents: `
|
|
const defaultAgent = new HttpAgent({
|
|
url: "http://localhost:8000/",
|
|
});
|
|
new CopilotRuntime({ agents: { default: defaultAgent } });
|
|
`,
|
|
contract: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
defect: "constructed agent is no longer registered",
|
|
contents: `
|
|
const defaultAgent = new HttpAgent({
|
|
url: process.env.AGENT_URL || "http://localhost:8000/",
|
|
});
|
|
new CopilotRuntime({ agents: { default: unregisteredAgent } });
|
|
`,
|
|
contract: HTTP_LOCALHOST_SLASH_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
{
|
|
defect: "framework factory changes",
|
|
contents: `
|
|
new CopilotRuntime({
|
|
agents: MastraAgent.getRemoteAgents({ mastra }),
|
|
});
|
|
`,
|
|
contract: MASTRA_RUNTIME_AGENT_CONTRACT,
|
|
},
|
|
])(
|
|
"the ordinary Runtime agent helper rejects when $defect",
|
|
({ contents, contract }) => {
|
|
expect(() => expectRuntimeAgentContract(contents, contract)).toThrow();
|
|
},
|
|
);
|
|
|
|
for (const contract of INTELLIGENCE_TEMPLATE_CONTRACTS) {
|
|
if ("runtimeAgent" in contract) {
|
|
test(`${contract.directory} runtime preserves its framework-specific agent wiring`, () => {
|
|
const runtime = readManagedSurface(
|
|
contract,
|
|
contract.runtimePath,
|
|
"runtime",
|
|
);
|
|
const agent =
|
|
contract.directory === "langgraph-fastapi"
|
|
? runtime
|
|
: readManagedSurface(contract, "src/agent.ts", "agent definition");
|
|
|
|
expectRuntimeAgentContract(runtime, contract.runtimeAgent, agent);
|
|
});
|
|
}
|
|
|
|
if (contract.directory !== "agentcore") {
|
|
test(`${contract.directory} runtime preserves all REST endpoint handlers`, () => {
|
|
const runtime = readManagedSurface(
|
|
contract,
|
|
contract.runtimePath,
|
|
"runtime",
|
|
);
|
|
|
|
expectEndpointHandlerContract(runtime);
|
|
});
|
|
}
|
|
|
|
test(`${contract.directory} frontend preserves REST transport and shared thread context`, () => {
|
|
const frontendPaths = frontendBehaviorPaths(contract);
|
|
const provider = readManagedSurface(
|
|
contract,
|
|
frontendPaths.providerPath,
|
|
"frontend provider",
|
|
);
|
|
const threadSurface = readManagedSurface(
|
|
contract,
|
|
frontendPaths.threadPath,
|
|
"thread surface",
|
|
);
|
|
|
|
expectFrontendThreadContract(provider, threadSurface);
|
|
});
|
|
|
|
if (contract.directory !== "agentcore") {
|
|
test(`${contract.directory} uses the no-token managed path with structured entitlements`, () => {
|
|
const runtime = readManagedSurface(
|
|
contract,
|
|
contract.runtimePath,
|
|
"runtime",
|
|
);
|
|
const gate = readManagedSurface(
|
|
contract,
|
|
contract.gatePath,
|
|
"client gate",
|
|
);
|
|
const readme = readManagedSurface(
|
|
contract,
|
|
contract.readmePath,
|
|
"README",
|
|
);
|
|
const pins = readManagedSdkPins(contract);
|
|
|
|
expect(pins.every(pinSupportsManagedEntitlements)).toBe(true);
|
|
expectManagedRuntimeContract(runtime);
|
|
expect(runtime).not.toMatch(
|
|
exactEnvIdentifierPattern(MANAGED_LICENSE_TOKEN),
|
|
);
|
|
expectManagedGateContract(gate);
|
|
for (const contents of [runtime, gate, readme]) {
|
|
expect(contents).not.toMatch(
|
|
exactEnvIdentifierPattern(OPTIONAL_TELEMETRY_ID),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (contract.directory === "agentcore") {
|
|
test(`${contract.directory} runtime uses the managed Intelligence API key`, () => {
|
|
const runtime = readManagedSurface(
|
|
contract,
|
|
contract.runtimePath,
|
|
"runtime",
|
|
);
|
|
|
|
expectManagedRuntimeContract(runtime);
|
|
});
|
|
|
|
test(`${contract.directory} env example documents managed Intelligence credentials`, () => {
|
|
const envExample = readManagedSurface(
|
|
contract,
|
|
contract.envPath,
|
|
"env example",
|
|
);
|
|
|
|
expectManagedEnvContract(envExample);
|
|
});
|
|
|
|
test(`${contract.directory} README documents managed Intelligence credentials`, () => {
|
|
const readme = readManagedSurface(
|
|
contract,
|
|
contract.readmePath,
|
|
"README",
|
|
);
|
|
|
|
expectManagedReadmeContract(readme, false);
|
|
});
|
|
}
|
|
|
|
if (contract.directory === "mcp-apps") {
|
|
test("mcp-apps runtime preserves its MCP Apps client middleware", () => {
|
|
const agent = readManagedSurface(
|
|
contract,
|
|
"app/agent.ts",
|
|
"agent definition",
|
|
);
|
|
|
|
expectMcpAppsRuntimeBehavior(agent);
|
|
});
|
|
|
|
test("mcp-apps preserves its streamable HTTP tools and UI resource server", () => {
|
|
const server = readManagedSurface(
|
|
contract,
|
|
"threejs-server/server.ts",
|
|
"MCP server",
|
|
);
|
|
const transport = readManagedSurface(
|
|
contract,
|
|
"threejs-server/server-utils.ts",
|
|
"MCP transport",
|
|
);
|
|
|
|
expectMcpServerBehavior(server, transport);
|
|
});
|
|
}
|
|
|
|
if (contract.directory === "a2a-middleware") {
|
|
test("a2a-middleware preserves isolated multi-agent routing", () => {
|
|
const agent = readManagedSurface(
|
|
contract,
|
|
"app/agent.ts",
|
|
"agent definition",
|
|
);
|
|
|
|
expectA2ARuntimeBehavior(agent);
|
|
});
|
|
|
|
test("a2a-middleware preserves its configured chat visualization tool", () => {
|
|
const chat = readManagedSurface(
|
|
contract,
|
|
"components/chat.tsx",
|
|
"A2A chat",
|
|
);
|
|
|
|
expectA2AVisualizationBehavior(chat);
|
|
});
|
|
}
|
|
|
|
if ("supportedPaths" in contract) {
|
|
const supportedPaths = contract.supportedPaths;
|
|
test(`${contract.directory} runtime preserves AgentCore bridge behavior`, () => {
|
|
const runtime = readManagedSurface(
|
|
contract,
|
|
contract.runtimePath,
|
|
"runtime",
|
|
);
|
|
|
|
expectAgentCoreRuntimeBehavior(runtime);
|
|
});
|
|
|
|
test(`${contract.directory} local Compose preserves service networking`, () => {
|
|
const compose = readManagedSurface(
|
|
contract,
|
|
supportedPaths.localComposePath,
|
|
"local Compose config",
|
|
);
|
|
|
|
expectAgentCoreNetworkingBehavior(compose);
|
|
});
|
|
|
|
test(`${contract.directory} deploy scripts preserve LangGraph and Strands variants`, () => {
|
|
const variants = [
|
|
{
|
|
path: supportedPaths.deployScriptPaths[0]!,
|
|
pattern: "langgraph-single-agent",
|
|
suffix: "-lg",
|
|
},
|
|
{
|
|
path: supportedPaths.deployScriptPaths[1]!,
|
|
pattern: "strands-single-agent",
|
|
suffix: "-st",
|
|
},
|
|
];
|
|
|
|
for (const variant of variants) {
|
|
const deployScript = readManagedSurface(
|
|
contract,
|
|
variant.path,
|
|
"deploy script",
|
|
);
|
|
expectAgentCoreVariantBehavior(
|
|
deployScript,
|
|
variant.pattern,
|
|
variant.suffix,
|
|
);
|
|
expect(
|
|
fs.existsSync(
|
|
path.join(
|
|
integrationsDir,
|
|
contract.directory,
|
|
"agents",
|
|
variant.pattern,
|
|
),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
test(`${contract.directory} local Compose consumes the root managed env`, () => {
|
|
const compose = readManagedSurface(
|
|
contract,
|
|
supportedPaths.localComposePath,
|
|
"local Compose config",
|
|
);
|
|
|
|
expectAgentCoreLocalComposeContract(compose);
|
|
});
|
|
|
|
test(`${contract.directory} deployment config stores a managed key secret reference`, () => {
|
|
const deploymentConfig = readManagedSurface(
|
|
contract,
|
|
supportedPaths.deploymentConfigPath,
|
|
"deployment config",
|
|
);
|
|
|
|
expectAgentCoreDeploymentConfigContract(deploymentConfig);
|
|
});
|
|
|
|
test(`${contract.directory} deployed Lambda resolves the managed key secret`, () => {
|
|
const runtimeDeployment = readManagedSurface(
|
|
contract,
|
|
supportedPaths.runtimeDeploymentPath,
|
|
"runtime deployment config",
|
|
);
|
|
|
|
expectAgentCoreRuntimeDeploymentContract(runtimeDeployment);
|
|
});
|
|
|
|
test(`${contract.directory} excludes self-hosted licenses from managed AWS setup`, () => {
|
|
const deploymentConfig = readManagedSurface(
|
|
contract,
|
|
supportedPaths.deploymentConfigPath,
|
|
"deployment config",
|
|
);
|
|
const runtimeDeployment = readManagedSurface(
|
|
contract,
|
|
supportedPaths.runtimeDeploymentPath,
|
|
"runtime deployment config",
|
|
);
|
|
const deployScripts = supportedPaths.deployScriptPaths.map(
|
|
(deployScriptPath) =>
|
|
readManagedSurface(contract, deployScriptPath, "deploy script"),
|
|
);
|
|
|
|
expectAgentCoreNoLicenseTokenContract(
|
|
deploymentConfig,
|
|
runtimeDeployment,
|
|
deployScripts,
|
|
);
|
|
});
|
|
|
|
test(`${contract.directory} explicitly excludes managed credentials from Terraform`, () => {
|
|
const terraformRuntimeDeployment = readManagedSurface(
|
|
contract,
|
|
supportedPaths.terraformRuntimeDeploymentPath,
|
|
"Terraform runtime deployment",
|
|
);
|
|
const terraformReadme = readManagedSurface(
|
|
contract,
|
|
supportedPaths.terraformReadmePath,
|
|
"Terraform README",
|
|
);
|
|
|
|
expectAgentCoreTerraformExclusionContract(
|
|
terraformRuntimeDeployment,
|
|
terraformReadme,
|
|
);
|
|
});
|
|
|
|
test(`${contract.directory} deploy scripts materialize the configured managed key secret`, () => {
|
|
for (const deployScriptPath of supportedPaths.deployScriptPaths) {
|
|
const deployScript = readManagedSurface(
|
|
contract,
|
|
deployScriptPath,
|
|
"deploy script",
|
|
);
|
|
expectAgentCoreDeployScriptContract(deployScript);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Assert AgentCore's AWS instructions document optional remote overrides. */
|
|
function expectAgentCoreAwsEndpointReadmeContract(contents: string): void {
|
|
expect(contents).toMatch(/Managed Intelligence uses its default endpoints/i);
|
|
expect(contents).toMatch(/For self-hosted\s+Intelligence/i);
|
|
expect(contents).toMatch(/endpoint overrides that AWS can reach/i);
|
|
expect(contents).toMatch(
|
|
/do not use\s+`localhost`[\s\S]{0,40}`127\.0\.0\.1`/i,
|
|
);
|
|
|
|
for (const scriptName of [
|
|
"deploy-langgraph.sh",
|
|
"deploy-strands.sh",
|
|
] as const) {
|
|
expect(contents).toMatch(
|
|
new RegExp(
|
|
`INTELLIGENCE_API_URL=https://[^\\s]+[\\s\\S]{0,200}` +
|
|
`INTELLIGENCE_GATEWAY_WS_URL=wss://[^\\s]+[\\s\\S]{0,200}` +
|
|
`\\./${scriptName.replace(".", "\\.")}`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Assert one AgentCore deploy script validates remote endpoints before AWS writes. */
|
|
function expectAgentCoreAwsEndpointDeployContract(contents: string): void {
|
|
const normalized = contents.replace(/\\\r?\n\s*/g, " ");
|
|
const lines = normalized.split(/\r?\n/);
|
|
const rootEnvLoadIndex = lines.findIndex((line) =>
|
|
/(?:^|\s)(?:source|\.)\s+["']?\$\{?SCRIPT_DIR\}?\/\.env["']?(?:\s|$)/.test(
|
|
line,
|
|
),
|
|
);
|
|
const apiGuardIndex = lines.findIndex((line) =>
|
|
/validate_remote_override\s+INTELLIGENCE_API_URL\b/.test(line),
|
|
);
|
|
const gatewayGuardIndex = lines.findIndex((line) =>
|
|
/validate_remote_override\s+INTELLIGENCE_GATEWAY_WS_URL\b/.test(line),
|
|
);
|
|
const firstAwsWriteIndex = lines.findIndex((line) =>
|
|
/aws\s+secretsmanager\s+(?:create-secret|put-secret-value)\b/.test(line),
|
|
);
|
|
const cdkDeployIndex = lines.findIndex((line) =>
|
|
/npx\s+cdk(?:@\S+)?\s+deploy\b/.test(line),
|
|
);
|
|
|
|
expect(contents).toContain("${INTELLIGENCE_API_URL+x}");
|
|
expect(contents).toContain("${INTELLIGENCE_GATEWAY_WS_URL+x}");
|
|
expect(contents).toContain("localhost");
|
|
expect(contents).toContain("127\\.0\\.0\\.1");
|
|
expect(apiGuardIndex).toBeGreaterThan(rootEnvLoadIndex);
|
|
expect(gatewayGuardIndex).toBeGreaterThan(rootEnvLoadIndex);
|
|
expect(firstAwsWriteIndex).toBeGreaterThan(
|
|
Math.max(apiGuardIndex, gatewayGuardIndex),
|
|
);
|
|
expect(cdkDeployIndex).toBeGreaterThan(
|
|
Math.max(apiGuardIndex, gatewayGuardIndex),
|
|
);
|
|
}
|
|
|
|
/** Write one executable command stub into an AgentCore deploy harness. */
|
|
function writeAgentCoreCommandStub(
|
|
directory: string,
|
|
command: string,
|
|
contents = "#!/usr/bin/env bash\nexit 0\n",
|
|
): void {
|
|
const commandPath = path.join(directory, command);
|
|
fs.writeFileSync(commandPath, contents);
|
|
fs.chmodSync(commandPath, 0o755);
|
|
}
|
|
|
|
/** Render one optional endpoint into a shell environment assignment. */
|
|
function agentCoreEndpointAssignment(name: string, value?: string): string {
|
|
return value === undefined ? "" : `${name}=${value}\n`;
|
|
}
|
|
|
|
/** Read one optional newline-delimited harness capture. */
|
|
function readAgentCoreHarnessLines(capturePath: string): string[] {
|
|
if (!fs.existsSync(capturePath)) {
|
|
return [];
|
|
}
|
|
return fs
|
|
.readFileSync(capturePath, "utf8")
|
|
.split("\n")
|
|
.filter((line) => line.length > 0);
|
|
}
|
|
|
|
/** Emit a shell probe that records any backend-credential environment leak. */
|
|
function agentCoreSecretExposureProbe(command: string): string {
|
|
return [
|
|
`if [[ -n "\${${MANAGED_API_KEY}-}" ]]; then`,
|
|
` printf '%s\\n' '${command}:env' >> "\${SECRET_EXPOSURE_CAPTURE:?}"`,
|
|
"fi",
|
|
`if [[ "$*" == *'${MANAGED_API_KEY_SENTINEL}'* ]]; then`,
|
|
` printf '%s\\n' '${command}:argv' >> "\${SECRET_EXPOSURE_CAPTURE:?}"`,
|
|
"fi",
|
|
].join("\n");
|
|
}
|
|
|
|
/** Execute one real AgentCore deploy script against non-networking command stubs. */
|
|
function runAgentCoreDeployHarness(
|
|
options: AgentCoreDeployHarnessOptions,
|
|
): AgentCoreDeployHarnessResult {
|
|
const harnessDirectory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), "agentcore-deploy-contract-"),
|
|
);
|
|
try {
|
|
const fakeBin = path.join(harnessDirectory, "bin");
|
|
const cdkDirectory = path.join(harnessDirectory, "infra-cdk");
|
|
const capturePath = path.join(harnessDirectory, "cdk-environment.txt");
|
|
const awsCapturePath = path.join(harnessDirectory, "aws-commands.txt");
|
|
const npmCapturePath = path.join(harnessDirectory, "npm-commands.txt");
|
|
const frontendCapturePath = path.join(
|
|
harnessDirectory,
|
|
"frontend-environment.txt",
|
|
);
|
|
const secretExposureCapturePath = path.join(
|
|
harnessDirectory,
|
|
"secret-exposure.txt",
|
|
);
|
|
const secretInputCapturePath = path.join(
|
|
harnessDirectory,
|
|
"secret-input.txt",
|
|
);
|
|
const scriptPath = path.join(harnessDirectory, options.scriptName);
|
|
fs.mkdirSync(fakeBin);
|
|
fs.mkdirSync(cdkDirectory);
|
|
fs.mkdirSync(path.join(harnessDirectory, "scripts"));
|
|
fs.copyFileSync(
|
|
path.join(integrationsDir, "agentcore", options.scriptName),
|
|
scriptPath,
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(harnessDirectory, ".env"),
|
|
[
|
|
`${MANAGED_API_KEY}=${MANAGED_API_KEY_SENTINEL}\n`,
|
|
`${OPTIONAL_TELEMETRY_ID}=agentcore-test-telemetry\n`,
|
|
agentCoreEndpointAssignment(
|
|
INTELLIGENCE_API_URL,
|
|
options.envFile.apiUrl,
|
|
),
|
|
agentCoreEndpointAssignment(
|
|
INTELLIGENCE_GATEWAY_WS_URL,
|
|
options.envFile.gatewayWsUrl,
|
|
),
|
|
].join(""),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(harnessDirectory, "config.yaml"),
|
|
[
|
|
"stack_name_base: agentcore-contract\n",
|
|
`${MANAGED_API_KEY_SECRET_CONFIG}: agentcore/contract/key\n`,
|
|
"backend:\n",
|
|
" pattern: placeholder\n",
|
|
].join(""),
|
|
);
|
|
|
|
for (const command of ["docker", "node"]) {
|
|
writeAgentCoreCommandStub(fakeBin, command);
|
|
}
|
|
writeAgentCoreCommandStub(
|
|
fakeBin,
|
|
"uv",
|
|
[
|
|
"#!/usr/bin/env bash\n",
|
|
"set -eu\n",
|
|
'[[ "${1-}" == "run" ]] && shift\n',
|
|
'if [[ "${1-}" == "--project" ]]; then shift 2; fi\n',
|
|
'if [[ "${1-}" == "python" ]]; then shift; exec python3 "$@"; fi\n',
|
|
'if [[ "${1-}" == *.py ]]; then exec python3 "$@"; fi\n',
|
|
'exec "$@"\n',
|
|
].join(""),
|
|
);
|
|
writeAgentCoreCommandStub(
|
|
fakeBin,
|
|
"npm",
|
|
[
|
|
"#!/usr/bin/env bash\n",
|
|
"set -eu\n",
|
|
`${agentCoreSecretExposureProbe("npm")}\n`,
|
|
'printf \'%s\\n\' "$*" >> "${NPM_COMMAND_CAPTURE:?}"\n',
|
|
].join(""),
|
|
);
|
|
writeAgentCoreCommandStub(
|
|
fakeBin,
|
|
"aws",
|
|
[
|
|
"#!/usr/bin/env bash\n",
|
|
"set -eu\n",
|
|
`${agentCoreSecretExposureProbe("aws")}\n`,
|
|
'printf \'%s\\n\' "$*" >> "${AWS_COMMAND_CAPTURE:?}"\n',
|
|
'if [[ "$*" == *"secretsmanager put-secret-value"* || "$*" == *"secretsmanager create-secret"* ]]; then\n',
|
|
" secret_input=$(cat)\n",
|
|
` if [[ "$secret_input" == '${MANAGED_API_KEY_SENTINEL}' ]]; then\n`,
|
|
" printf '%s\\n' 'aws:key-stdin-match' >> \"${SECRET_INPUT_CAPTURE:?}\"\n",
|
|
` printf '%s\\n' '${MANAGED_API_KEY_SECRET_VERSION_SENTINEL}'\n`,
|
|
" else\n",
|
|
" printf '%s\\n' 'aws:stdin-missing' >> \"${SECRET_INPUT_CAPTURE:?}\"\n",
|
|
" fi\n",
|
|
"fi\n",
|
|
].join(""),
|
|
);
|
|
writeAgentCoreCommandStub(
|
|
fakeBin,
|
|
"npx",
|
|
[
|
|
"#!/usr/bin/env bash\n",
|
|
"set -eu\n",
|
|
`${agentCoreSecretExposureProbe("npx")}\n`,
|
|
`printf '%s\\n%s\\n%s\\n' "\${${INTELLIGENCE_API_URL}-}" "\${${INTELLIGENCE_GATEWAY_WS_URL}-}" "\${${MANAGED_API_KEY_SECRET_VERSION_ID}-}" > "\${CDK_ENV_CAPTURE:?}"\n`,
|
|
].join(""),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(harnessDirectory, "scripts", "deploy-frontend.py"),
|
|
[
|
|
"import os\n",
|
|
"import pathlib\n",
|
|
"import sys\n",
|
|
`if os.environ.get('${MANAGED_API_KEY}'):\n`,
|
|
' with pathlib.Path(os.environ["SECRET_EXPOSURE_CAPTURE"]).open("a") as capture:\n',
|
|
' capture.write("frontend:env\\n")\n',
|
|
'pathlib.Path(os.environ["FRONTEND_CAPTURE"]).write_text(\n',
|
|
` f"{sys.argv[1]}\\n{os.environ.get('${MANAGED_API_KEY_SECRET_VERSION_ID}', '')}\\n"\n`,
|
|
")\n",
|
|
].join(""),
|
|
);
|
|
|
|
const environment = { ...process.env };
|
|
environment[MANAGED_API_KEY] = MANAGED_API_KEY_SENTINEL;
|
|
delete environment[INTELLIGENCE_API_URL];
|
|
delete environment[INTELLIGENCE_GATEWAY_WS_URL];
|
|
delete environment[MANAGED_API_KEY_SECRET_VERSION_ID];
|
|
environment.PATH = `${fakeBin}:${process.env.PATH ?? ""}`;
|
|
environment.CDK_ENV_CAPTURE = capturePath;
|
|
environment.AWS_COMMAND_CAPTURE = awsCapturePath;
|
|
environment.NPM_COMMAND_CAPTURE = npmCapturePath;
|
|
environment.FRONTEND_CAPTURE = frontendCapturePath;
|
|
environment.SECRET_EXPOSURE_CAPTURE = secretExposureCapturePath;
|
|
environment.SECRET_INPUT_CAPTURE = secretInputCapturePath;
|
|
if (options.callerEnvironment?.apiUrl !== undefined) {
|
|
environment[INTELLIGENCE_API_URL] = options.callerEnvironment.apiUrl;
|
|
}
|
|
if (options.callerEnvironment?.gatewayWsUrl !== undefined) {
|
|
environment[INTELLIGENCE_GATEWAY_WS_URL] =
|
|
options.callerEnvironment.gatewayWsUrl;
|
|
}
|
|
|
|
const scriptArguments = [
|
|
...(options.xtrace === true ? ["-x"] : []),
|
|
scriptPath,
|
|
...(options.skipFrontend === false ? [] : ["--skip-frontend"]),
|
|
...(options.skipBackend === true ? ["--skip-backend"] : []),
|
|
];
|
|
const result = spawnSync("/bin/bash", scriptArguments, {
|
|
cwd: harnessDirectory,
|
|
encoding: "utf8",
|
|
env: environment,
|
|
});
|
|
const output = `${result.stdout}${result.stderr}`;
|
|
const capturedEnvironment = fs.existsSync(capturePath)
|
|
? fs.readFileSync(capturePath, "utf8")
|
|
: null;
|
|
const capturedLines = capturedEnvironment?.split("\n") ?? [];
|
|
const cdkEnvironment = capturedEnvironment
|
|
? `${capturedLines[0] ?? ""}\n${capturedLines[1] ?? ""}\n`
|
|
: null;
|
|
const secretVersionId = capturedEnvironment
|
|
? (capturedLines[2] ?? null)
|
|
: null;
|
|
const frontendEnvironment = fs.existsSync(frontendCapturePath)
|
|
? fs.readFileSync(frontendCapturePath, "utf8").split("\n")
|
|
: null;
|
|
return {
|
|
status: result.status,
|
|
output,
|
|
cdkEnvironment,
|
|
secretVersionId,
|
|
awsCommands: readAgentCoreHarnessLines(awsCapturePath),
|
|
npmCommands: readAgentCoreHarnessLines(npmCapturePath),
|
|
frontendStackName: frontendEnvironment?.[0] ?? null,
|
|
frontendSecretVersionId: frontendEnvironment?.[1] ?? null,
|
|
secretExposureObservations: readAgentCoreHarnessLines(
|
|
secretExposureCapturePath,
|
|
),
|
|
secretInputObservations: readAgentCoreHarnessLines(
|
|
secretInputCapturePath,
|
|
),
|
|
};
|
|
} finally {
|
|
fs.rmSync(harnessDirectory, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mastra working-memory scope contract.
|
|
*
|
|
* `@ag-ui/mastra` writes the client's shared state into Mastra working memory
|
|
* BEFORE it streams a turn (`syncInputStateToWorkingMemory`). That write is
|
|
* unguarded and never creates the thread, because it assumes the resource-scoped
|
|
* store, which upserts. Under `scope: "thread"` the same write goes to thread
|
|
* metadata instead, and `@mastra/memory` throws `Thread <id> not found` when the
|
|
* thread row does not exist yet. On the first turn of a thread it never does, so
|
|
* the run aborts before the model is called and the chat never answers.
|
|
*
|
|
* Managed Intelligence makes that certain rather than likely: the Intelligence
|
|
* run handler replaces the client thread id with a platform-canonical one, which
|
|
* the Mastra store has never seen.
|
|
*
|
|
* Resource scope is the correct setting for these starters and keeps working
|
|
* memory per conversation, because the bridge derives the resource id from the
|
|
* thread id when no explicit resource id is configured.
|
|
*/
|
|
const MASTRA_MEMORY_GLOB_DIRECTORIES = ["src/mastra"] as const;
|
|
|
|
/** Returns every `.ts` file under one directory, recursively. */
|
|
function typeScriptFilesUnder(directory: string): string[] {
|
|
if (!fs.existsSync(directory)) return [];
|
|
|
|
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const entryPath = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) return typeScriptFilesUnder(entryPath);
|
|
return entry.isFile() && entryPath.endsWith(".ts") ? [entryPath] : [];
|
|
});
|
|
}
|
|
|
|
/** Returns every Mastra source file across the integration starters. */
|
|
function mastraMemorySurfaces(): string[] {
|
|
return fs
|
|
.readdirSync(integrationsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.flatMap((entry) =>
|
|
MASTRA_MEMORY_GLOB_DIRECTORIES.flatMap((relative) =>
|
|
typeScriptFilesUnder(path.join(integrationsDir, entry.name, relative)),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether one source configures thread-scoped Mastra working memory.
|
|
*
|
|
* @param contents - TypeScript source to inspect.
|
|
* @returns True when any `workingMemory` object sets `scope: "thread"`.
|
|
*/
|
|
function declaresThreadScopedWorkingMemory(contents: string): boolean {
|
|
const sourceFile = parseManagedSource(contents);
|
|
let found = false;
|
|
|
|
const visit = (node: ts.Node): void => {
|
|
if (found) return;
|
|
|
|
if (
|
|
ts.isPropertyAssignment(node) &&
|
|
propertyNameText(node.name) === "workingMemory"
|
|
) {
|
|
const initializer = unwrapExpression(node.initializer);
|
|
if (
|
|
ts.isObjectLiteralExpression(initializer) &&
|
|
objectPropertyIsString(initializer, "scope", "thread")
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
|
|
ts.forEachChild(sourceFile, visit);
|
|
|
|
return found;
|
|
}
|
|
|
|
test("integration starters never configure thread-scoped Mastra working memory", () => {
|
|
const surfaces = mastraMemorySurfaces();
|
|
|
|
// Guards against a vacuous pass: the mastra starter must be in scope.
|
|
expect(
|
|
surfaces.some((surface) =>
|
|
surface.includes(path.join("mastra", "src", "mastra")),
|
|
),
|
|
"the mastra starter's Mastra sources must be scanned",
|
|
).toBe(true);
|
|
|
|
const offenders = surfaces.filter((surface) =>
|
|
declaresThreadScopedWorkingMemory(fs.readFileSync(surface, "utf8")),
|
|
);
|
|
|
|
expect(
|
|
offenders.map((surface) => path.relative(integrationsDir, surface)),
|
|
"thread-scoped working memory aborts the first turn of every new thread",
|
|
).toEqual([]);
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
configuration: "thread scope",
|
|
contents: `
|
|
new Memory({
|
|
options: { workingMemory: { enabled: true, scope: "thread" } },
|
|
});
|
|
`,
|
|
expected: true,
|
|
},
|
|
{
|
|
configuration: "resource scope",
|
|
contents: `
|
|
new Memory({
|
|
options: { workingMemory: { enabled: true, scope: "resource" } },
|
|
});
|
|
`,
|
|
expected: false,
|
|
},
|
|
{
|
|
configuration: "an omitted scope",
|
|
contents: `
|
|
new Memory({ options: { workingMemory: { enabled: true } } });
|
|
`,
|
|
expected: false,
|
|
},
|
|
{
|
|
configuration: "thread scope nested below other options",
|
|
contents: `
|
|
new Agent({
|
|
memory: new Memory({
|
|
options: {
|
|
workingMemory: { enabled: true, schema: AgentState, scope: "thread" },
|
|
},
|
|
}),
|
|
});
|
|
`,
|
|
expected: true,
|
|
},
|
|
{
|
|
configuration: "an unrelated thread-scoped option",
|
|
contents: `
|
|
new Memory({
|
|
options: { semanticRecall: { scope: "thread" } },
|
|
});
|
|
`,
|
|
expected: false,
|
|
},
|
|
])(
|
|
"the Mastra working-memory scope helper detects $configuration",
|
|
({ contents, expected }) => {
|
|
expect(declaresThreadScopedWorkingMemory(contents)).toBe(expected);
|
|
},
|
|
);
|