@app-vitals/agent-admin 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +64 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-manifest.unit.test.ts
|
|
3
|
+
* Unit tests for the pure agent Kubernetes manifest builders. No I/O, no
|
|
4
|
+
* network, no fs — these assert wire-shape only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
AGENT_HEALTH_PORT,
|
|
10
|
+
AGENT_HOME_MOUNT_PATH,
|
|
11
|
+
type AgentContainerResourceOverrides,
|
|
12
|
+
type AgentDeploymentOpts,
|
|
13
|
+
type AgentPvcOpts,
|
|
14
|
+
type AgentSecretOpts,
|
|
15
|
+
buildAgentDeploymentManifest,
|
|
16
|
+
buildAgentPvcManifest,
|
|
17
|
+
buildAgentSecretManifest,
|
|
18
|
+
resolveAgentContainerResources,
|
|
19
|
+
sanitizeAgentName,
|
|
20
|
+
TENANT_LABEL,
|
|
21
|
+
} from "./agent-manifest.ts";
|
|
22
|
+
|
|
23
|
+
// ─── Shared fixtures ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const deployOpts: AgentDeploymentOpts = {
|
|
26
|
+
agentId: "agent_42",
|
|
27
|
+
namespace: "shipwright",
|
|
28
|
+
image: "ghcr.io/app-vitals/shipwright-agent",
|
|
29
|
+
imageTag: "v1.2.3",
|
|
30
|
+
apiUrl: "http://shipwright-admin.shipwright.svc:3001",
|
|
31
|
+
pvcName: "agent-42-home",
|
|
32
|
+
secretName: "agent-42-token",
|
|
33
|
+
tokenSecretKey: "token",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ─── buildAgentDeploymentManifest ───────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("buildAgentDeploymentManifest", () => {
|
|
39
|
+
it("emits a valid apps/v1 Deployment", () => {
|
|
40
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
41
|
+
expect(d.apiVersion).toBe("apps/v1");
|
|
42
|
+
expect(d.kind).toBe("Deployment");
|
|
43
|
+
expect(d.metadata.namespace).toBe("shipwright");
|
|
44
|
+
expect(d.spec.replicas).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("derives a sanitized RFC1123 metadata name from the agent id", () => {
|
|
48
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
49
|
+
expect(d.metadata.name).toBe(sanitizeAgentName("agent_42"));
|
|
50
|
+
expect(d.metadata.name).not.toContain("_");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("joins image and tag", () => {
|
|
54
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
55
|
+
expect(d.spec.template.spec.containers[0].image).toBe(
|
|
56
|
+
"ghcr.io/app-vitals/shipwright-agent:v1.2.3",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("sets labels and matching selector/template labels", () => {
|
|
61
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
62
|
+
const name = sanitizeAgentName("agent_42");
|
|
63
|
+
expect(d.metadata.labels).toMatchObject({
|
|
64
|
+
"app.kubernetes.io/name": "shipwright-agent",
|
|
65
|
+
"app.kubernetes.io/instance": name,
|
|
66
|
+
"shipwright.dev/agent-id": "agent_42",
|
|
67
|
+
});
|
|
68
|
+
// selector must match the pod template labels (else 0 pods scheduled)
|
|
69
|
+
expect(d.spec.selector.matchLabels).toEqual(
|
|
70
|
+
d.spec.template.metadata.labels,
|
|
71
|
+
);
|
|
72
|
+
expect(d.spec.selector.matchLabels).toMatchObject({
|
|
73
|
+
"app.kubernetes.io/instance": name,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("sets the required env vars including a secretKeyRef token", () => {
|
|
78
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
79
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
80
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
81
|
+
|
|
82
|
+
expect(byName.get("SHIPWRIGHT_AGENT_ID")?.value).toBe("agent_42");
|
|
83
|
+
expect(byName.get("SHIPWRIGHT_API_URL")?.value).toBe(
|
|
84
|
+
"http://shipwright-admin.shipwright.svc:3001",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const tokenEnv = byName.get("SHIPWRIGHT_AGENT_API_KEY");
|
|
88
|
+
expect(tokenEnv).toBeDefined();
|
|
89
|
+
expect(tokenEnv?.value).toBeUndefined();
|
|
90
|
+
expect(tokenEnv?.valueFrom?.secretKeyRef).toEqual({
|
|
91
|
+
name: "agent-42-token",
|
|
92
|
+
key: "token",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("mounts a PVC volume at the agent home path", () => {
|
|
97
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
98
|
+
const podSpec = d.spec.template.spec;
|
|
99
|
+
|
|
100
|
+
const vol = podSpec.volumes?.find(
|
|
101
|
+
(v) => v.persistentVolumeClaim?.claimName === "agent-42-home",
|
|
102
|
+
);
|
|
103
|
+
expect(vol).toBeDefined();
|
|
104
|
+
|
|
105
|
+
const mount = podSpec.containers[0].volumeMounts?.find(
|
|
106
|
+
(m) => m.mountPath === AGENT_HOME_MOUNT_PATH,
|
|
107
|
+
);
|
|
108
|
+
expect(mount).toBeDefined();
|
|
109
|
+
expect(AGENT_HOME_MOUNT_PATH).toBe("/data/agent-home");
|
|
110
|
+
// volume name and mount name must agree
|
|
111
|
+
expect(mount?.name).toBe(vol?.name);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sets strategy Recreate", () => {
|
|
115
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
116
|
+
expect(d.spec.strategy).toEqual({ type: "Recreate" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("sets terminationGracePeriodSeconds to 120", () => {
|
|
120
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
121
|
+
expect(d.spec.template.spec.terminationGracePeriodSeconds).toBe(120);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("declares a containerPort for the health port", () => {
|
|
125
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
126
|
+
const ports = d.spec.template.spec.containers[0].ports ?? [];
|
|
127
|
+
expect(ports).toContainEqual({
|
|
128
|
+
containerPort: AGENT_HEALTH_PORT,
|
|
129
|
+
protocol: "TCP",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("sets AGENT_HOME env var to the mount path", () => {
|
|
134
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
135
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
136
|
+
const agentHome = env.find((e) => e.name === "AGENT_HOME");
|
|
137
|
+
expect(agentHome?.value).toBe(AGENT_HOME_MOUNT_PATH);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("routes repo clones and worktrees to the PVC, not ephemeral $HOME", () => {
|
|
141
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
142
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
143
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
144
|
+
expect(byName.get("SHIPWRIGHT_REPO_DIR")?.value).toBe(
|
|
145
|
+
`${AGENT_HOME_MOUNT_PATH}/workspace/repos`,
|
|
146
|
+
);
|
|
147
|
+
expect(byName.get("SHIPWRIGHT_WORKTREE_DIR")?.value).toBe(
|
|
148
|
+
`${AGENT_HOME_MOUNT_PATH}/workspace/worktrees`,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("sets explicit container resources with a memory limit by default (no overrides)", () => {
|
|
153
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
154
|
+
const resources = d.spec.template.spec.containers[0].resources;
|
|
155
|
+
expect(resources?.requests?.memory).toBe("2Gi");
|
|
156
|
+
expect(resources?.limits?.memory).toBe("8Gi");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("budgets 4Gi of ephemeral storage by default, matched across request and limit", () => {
|
|
160
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
161
|
+
const resources = d.spec.template.spec.containers[0].resources;
|
|
162
|
+
// 1Gi (the Autopilot default) evicted agents mid-run once a build step
|
|
163
|
+
// wrote any real scratch space. Request and limit stay equal — Autopilot
|
|
164
|
+
// bills the request regardless, so a lower request buys nothing.
|
|
165
|
+
expect(resources?.limits?.["ephemeral-storage"]).toBe("4Gi");
|
|
166
|
+
expect(resources?.requests?.["ephemeral-storage"]).toBe("4Gi");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("threads opts.resources overrides through into the manifest's container resources", () => {
|
|
170
|
+
const d = buildAgentDeploymentManifest({
|
|
171
|
+
...deployOpts,
|
|
172
|
+
resources: { cpuRequest: "2000m", memoryLimit: "16Gi" },
|
|
173
|
+
});
|
|
174
|
+
const resources = d.spec.template.spec.containers[0].resources;
|
|
175
|
+
expect(resources).toEqual({
|
|
176
|
+
requests: { cpu: "2000m", memory: "2Gi", "ephemeral-storage": "4Gi" },
|
|
177
|
+
limits: { memory: "16Gi", "ephemeral-storage": "4Gi" },
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("defines liveness and readiness probes on the health port", () => {
|
|
182
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
183
|
+
const c = d.spec.template.spec.containers[0];
|
|
184
|
+
expect(AGENT_HEALTH_PORT).toBe(3459);
|
|
185
|
+
expect(c.livenessProbe?.httpGet?.port).toBe(3459);
|
|
186
|
+
expect(c.livenessProbe?.failureThreshold).toBe(3);
|
|
187
|
+
expect(c.livenessProbe?.timeoutSeconds).toBe(15);
|
|
188
|
+
expect(c.readinessProbe?.httpGet?.port).toBe(3459);
|
|
189
|
+
expect(c.readinessProbe?.failureThreshold).toBe(3);
|
|
190
|
+
expect(c.readinessProbe?.timeoutSeconds).toBe(15);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("gates liveness/readiness with a startupProbe granting ~180s for a slow mise startup", () => {
|
|
194
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
195
|
+
const c = d.spec.template.spec.containers[0];
|
|
196
|
+
expect(c.startupProbe?.httpGet?.port).toBe(AGENT_HEALTH_PORT);
|
|
197
|
+
expect(c.startupProbe?.httpGet?.path).toBe("/health");
|
|
198
|
+
// periodSeconds × failureThreshold = total grace before liveness engages.
|
|
199
|
+
const grace =
|
|
200
|
+
(c.startupProbe?.periodSeconds as number) *
|
|
201
|
+
(c.startupProbe?.failureThreshold as number);
|
|
202
|
+
expect(grace).toBe(180);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("applies a hardened security context (fsGroup, fsGroupChangePolicy, runAsNonRoot, runAsUser)", () => {
|
|
206
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
207
|
+
const podSpec = d.spec.template.spec;
|
|
208
|
+
expect(podSpec.securityContext).toMatchObject({
|
|
209
|
+
fsGroup: 1000,
|
|
210
|
+
fsGroupChangePolicy: "OnRootMismatch",
|
|
211
|
+
runAsNonRoot: true,
|
|
212
|
+
runAsUser: 1000,
|
|
213
|
+
});
|
|
214
|
+
expect(podSpec.containers[0].securityContext).toMatchObject({
|
|
215
|
+
runAsNonRoot: true,
|
|
216
|
+
runAsUser: 1000,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does not set ownerReferences", () => {
|
|
221
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
222
|
+
expect(d.metadata.ownerReferences).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("honours an explicit replicas override", () => {
|
|
226
|
+
const d = buildAgentDeploymentManifest({ ...deployOpts, replicas: 0 });
|
|
227
|
+
expect(d.spec.replicas).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── resolveAgentContainerResources ────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
describe("resolveAgentContainerResources", () => {
|
|
234
|
+
// Byte-identical to today's values — this is the regression pin for AGR-7.1:
|
|
235
|
+
// no overrides must reproduce the exact object the old AGENT_CONTAINER_RESOURCES
|
|
236
|
+
// constant used to return.
|
|
237
|
+
const DEFAULT_RESOURCES = {
|
|
238
|
+
requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "4Gi" },
|
|
239
|
+
limits: { memory: "8Gi", "ephemeral-storage": "4Gi" },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
it("returns today's values byte-identical when called with no overrides", () => {
|
|
243
|
+
expect(resolveAgentContainerResources()).toEqual(DEFAULT_RESOURCES);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("returns today's values byte-identical when called with an empty overrides object", () => {
|
|
247
|
+
expect(resolveAgentContainerResources({})).toEqual(DEFAULT_RESOURCES);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("overrides only cpuRequest, leaving every other field at default", () => {
|
|
251
|
+
const r = resolveAgentContainerResources({ cpuRequest: "1000m" });
|
|
252
|
+
expect(r).toEqual({
|
|
253
|
+
requests: { cpu: "1000m", memory: "2Gi", "ephemeral-storage": "4Gi" },
|
|
254
|
+
limits: { memory: "8Gi", "ephemeral-storage": "4Gi" },
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("overrides only memoryRequest, leaving every other field at default", () => {
|
|
259
|
+
const r = resolveAgentContainerResources({ memoryRequest: "4Gi" });
|
|
260
|
+
expect(r).toEqual({
|
|
261
|
+
requests: { cpu: "500m", memory: "4Gi", "ephemeral-storage": "4Gi" },
|
|
262
|
+
limits: { memory: "8Gi", "ephemeral-storage": "4Gi" },
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("overrides only memoryLimit, leaving every other field at default", () => {
|
|
267
|
+
const r = resolveAgentContainerResources({ memoryLimit: "16Gi" });
|
|
268
|
+
expect(r).toEqual({
|
|
269
|
+
requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "4Gi" },
|
|
270
|
+
limits: { memory: "16Gi", "ephemeral-storage": "4Gi" },
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("overrides ephemeralStorage on BOTH request and limit, leaving cpu/memory at default", () => {
|
|
275
|
+
const r = resolveAgentContainerResources({ ephemeralStorage: "8Gi" });
|
|
276
|
+
expect(r).toEqual({
|
|
277
|
+
requests: { cpu: "500m", memory: "2Gi", "ephemeral-storage": "8Gi" },
|
|
278
|
+
limits: { memory: "8Gi", "ephemeral-storage": "8Gi" },
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("merges a partial combination of overrides with the remaining defaults", () => {
|
|
283
|
+
const r = resolveAgentContainerResources({
|
|
284
|
+
cpuRequest: "2000m",
|
|
285
|
+
memoryLimit: "12Gi",
|
|
286
|
+
});
|
|
287
|
+
expect(r).toEqual({
|
|
288
|
+
requests: { cpu: "2000m", memory: "2Gi", "ephemeral-storage": "4Gi" },
|
|
289
|
+
limits: { memory: "12Gi", "ephemeral-storage": "4Gi" },
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("applies all four overrides simultaneously", () => {
|
|
294
|
+
const overrides: AgentContainerResourceOverrides = {
|
|
295
|
+
cpuRequest: "1500m",
|
|
296
|
+
memoryRequest: "3Gi",
|
|
297
|
+
memoryLimit: "10Gi",
|
|
298
|
+
ephemeralStorage: "6Gi",
|
|
299
|
+
};
|
|
300
|
+
expect(resolveAgentContainerResources(overrides)).toEqual({
|
|
301
|
+
requests: { cpu: "1500m", memory: "3Gi", "ephemeral-storage": "6Gi" },
|
|
302
|
+
limits: { memory: "10Gi", "ephemeral-storage": "6Gi" },
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ─── voice env injection ────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
describe("buildAgentDeploymentManifest — voice env", () => {
|
|
310
|
+
const envNames = (d: ReturnType<typeof buildAgentDeploymentManifest>) =>
|
|
311
|
+
(d.spec.template.spec.containers[0].env ?? []).map((e) => e.name);
|
|
312
|
+
|
|
313
|
+
it("injects only the 6 base vars when no voice config is supplied (disabled)", () => {
|
|
314
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
315
|
+
expect(envNames(d)).toEqual([
|
|
316
|
+
"SHIPWRIGHT_AGENT_ID",
|
|
317
|
+
"SHIPWRIGHT_API_URL",
|
|
318
|
+
"SHIPWRIGHT_AGENT_API_KEY",
|
|
319
|
+
"AGENT_HOME",
|
|
320
|
+
"SHIPWRIGHT_REPO_DIR",
|
|
321
|
+
"SHIPWRIGHT_WORKTREE_DIR",
|
|
322
|
+
]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("injects WHISPER_SERVICE_URL + ELEVENLABS_API_KEY for the whisper provider", () => {
|
|
326
|
+
const d = buildAgentDeploymentManifest({
|
|
327
|
+
...deployOpts,
|
|
328
|
+
voice: {
|
|
329
|
+
whisperServiceUrl: "http://r-shipwright-whisper:9000",
|
|
330
|
+
elevenLabsApiKey: "el-key",
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
334
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
335
|
+
|
|
336
|
+
expect(byName.get("WHISPER_SERVICE_URL")?.value).toBe(
|
|
337
|
+
"http://r-shipwright-whisper:9000",
|
|
338
|
+
);
|
|
339
|
+
expect(byName.get("ELEVENLABS_API_KEY")?.value).toBe("el-key");
|
|
340
|
+
// whisper mode does not flow a Groq key
|
|
341
|
+
expect(byName.has("GROQ_API_KEY")).toBe(false);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("injects GROQ_API_KEY + ELEVENLABS_API_KEY for the groq provider (no WHISPER_SERVICE_URL)", () => {
|
|
345
|
+
const d = buildAgentDeploymentManifest({
|
|
346
|
+
...deployOpts,
|
|
347
|
+
voice: {
|
|
348
|
+
groqApiKey: "groq-key",
|
|
349
|
+
elevenLabsApiKey: "el-key",
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
353
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
354
|
+
|
|
355
|
+
expect(byName.get("GROQ_API_KEY")?.value).toBe("groq-key");
|
|
356
|
+
expect(byName.get("ELEVENLABS_API_KEY")?.value).toBe("el-key");
|
|
357
|
+
expect(byName.has("WHISPER_SERVICE_URL")).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("injects an optional ELEVENLABS_VOICE_ID when supplied", () => {
|
|
361
|
+
const d = buildAgentDeploymentManifest({
|
|
362
|
+
...deployOpts,
|
|
363
|
+
voice: {
|
|
364
|
+
elevenLabsApiKey: "el-key",
|
|
365
|
+
voiceId: "voice-xyz",
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
369
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
370
|
+
expect(byName.get("ELEVENLABS_VOICE_ID")?.value).toBe("voice-xyz");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("keeps the base 6 vars first and appends voice vars after them", () => {
|
|
374
|
+
const d = buildAgentDeploymentManifest({
|
|
375
|
+
...deployOpts,
|
|
376
|
+
voice: { whisperServiceUrl: "http://w:9000", elevenLabsApiKey: "k" },
|
|
377
|
+
});
|
|
378
|
+
const names = envNames(d);
|
|
379
|
+
expect(names.slice(0, 6)).toEqual([
|
|
380
|
+
"SHIPWRIGHT_AGENT_ID",
|
|
381
|
+
"SHIPWRIGHT_API_URL",
|
|
382
|
+
"SHIPWRIGHT_AGENT_API_KEY",
|
|
383
|
+
"AGENT_HOME",
|
|
384
|
+
"SHIPWRIGHT_REPO_DIR",
|
|
385
|
+
"SHIPWRIGHT_WORKTREE_DIR",
|
|
386
|
+
]);
|
|
387
|
+
expect(names).toContain("WHISPER_SERVICE_URL");
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ─── buildAgentSecretManifest ───────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
describe("buildAgentSecretManifest", () => {
|
|
394
|
+
const secretOpts: AgentSecretOpts = {
|
|
395
|
+
name: "agent-42-token",
|
|
396
|
+
namespace: "shipwright",
|
|
397
|
+
token: "sw-agent-secret-token",
|
|
398
|
+
tokenKey: "token",
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
it("emits an Opaque v1 Secret", () => {
|
|
402
|
+
const s = buildAgentSecretManifest(secretOpts);
|
|
403
|
+
expect(s.apiVersion).toBe("v1");
|
|
404
|
+
expect(s.kind).toBe("Secret");
|
|
405
|
+
expect(s.type).toBe("Opaque");
|
|
406
|
+
expect(s.metadata.name).toBe("agent-42-token");
|
|
407
|
+
expect(s.metadata.namespace).toBe("shipwright");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("base64-encodes the token under the configured key in data", () => {
|
|
411
|
+
const s = buildAgentSecretManifest(secretOpts);
|
|
412
|
+
expect(s.data.token).toBe(
|
|
413
|
+
Buffer.from("sw-agent-secret-token").toString("base64"),
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("defaults the token key to 'token'", () => {
|
|
418
|
+
const { tokenKey: _omit, ...rest } = secretOpts;
|
|
419
|
+
const s = buildAgentSecretManifest(rest);
|
|
420
|
+
expect(s.data.token).toBeDefined();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("does not set ownerReferences", () => {
|
|
424
|
+
const s = buildAgentSecretManifest(secretOpts);
|
|
425
|
+
expect(s.metadata.ownerReferences).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// ─── buildAgentPvcManifest ──────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
describe("buildAgentPvcManifest", () => {
|
|
432
|
+
const pvcOpts: AgentPvcOpts = {
|
|
433
|
+
name: "agent-42-home",
|
|
434
|
+
namespace: "shipwright",
|
|
435
|
+
sizeGi: 40,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
it("emits a v1 PersistentVolumeClaim", () => {
|
|
439
|
+
const p = buildAgentPvcManifest(pvcOpts);
|
|
440
|
+
expect(p.apiVersion).toBe("v1");
|
|
441
|
+
expect(p.kind).toBe("PersistentVolumeClaim");
|
|
442
|
+
expect(p.metadata.name).toBe("agent-42-home");
|
|
443
|
+
expect(p.metadata.namespace).toBe("shipwright");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("sets accessModes to ReadWriteOnce", () => {
|
|
447
|
+
const p = buildAgentPvcManifest(pvcOpts);
|
|
448
|
+
expect(p.spec.accessModes).toEqual(["ReadWriteOnce"]);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("sets storage to sizeGi Gi", () => {
|
|
452
|
+
const p = buildAgentPvcManifest(pvcOpts);
|
|
453
|
+
expect(p.spec.resources.requests.storage).toBe("40Gi");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("honours a custom sizeGi", () => {
|
|
457
|
+
const p = buildAgentPvcManifest({ ...pvcOpts, sizeGi: 100 });
|
|
458
|
+
expect(p.spec.resources.requests.storage).toBe("100Gi");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("includes storageClassName when provided", () => {
|
|
462
|
+
const p = buildAgentPvcManifest({
|
|
463
|
+
...pvcOpts,
|
|
464
|
+
storageClassName: "premium",
|
|
465
|
+
});
|
|
466
|
+
expect(p.spec.storageClassName).toBe("premium");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("omits storageClassName when not provided", () => {
|
|
470
|
+
const p = buildAgentPvcManifest(pvcOpts);
|
|
471
|
+
expect(p.spec.storageClassName).toBeUndefined();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("does not set ownerReferences", () => {
|
|
475
|
+
const p = buildAgentPvcManifest(pvcOpts);
|
|
476
|
+
expect(p.metadata.ownerReferences).toBeUndefined();
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ─── sanitizeAgentName ──────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
const RFC1123 = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
|
|
483
|
+
|
|
484
|
+
describe("sanitizeAgentName", () => {
|
|
485
|
+
it("lowercases uppercase characters (lossy → hash suffix)", () => {
|
|
486
|
+
expect(sanitizeAgentName("AgentFoo")).toMatch(RFC1123);
|
|
487
|
+
expect(sanitizeAgentName("AgentFoo")).toMatch(/^agentfoo-[a-z0-9]+$/);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("replaces underscores and dots with dashes (lossy → hash suffix)", () => {
|
|
491
|
+
// The cleaned base is "agent-42-beta", then a short hash of the original id
|
|
492
|
+
// is appended so distinct originals never collide.
|
|
493
|
+
expect(sanitizeAgentName("agent_42.beta")).toMatch(
|
|
494
|
+
/^agent-42-beta-[a-z0-9]+$/,
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("collapses runs of invalid characters into a single dash", () => {
|
|
499
|
+
expect(sanitizeAgentName("a___b")).toMatch(/^a-b-[a-z0-9]+$/);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("strips leading and trailing non-alphanumerics", () => {
|
|
503
|
+
expect(sanitizeAgentName("__agent__")).toMatch(/^agent-[a-z0-9]+$/);
|
|
504
|
+
expect(sanitizeAgentName("-.-foo-.-")).toMatch(/^foo-[a-z0-9]+$/);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("leaves an already-RFC1123 id unchanged (lossless → no hash suffix)", () => {
|
|
508
|
+
expect(sanitizeAgentName("agent-42")).toBe("agent-42");
|
|
509
|
+
expect(sanitizeAgentName("a1")).toBe("a1");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("always produces an RFC1123-compliant label", () => {
|
|
513
|
+
for (const id of [
|
|
514
|
+
"Agent_42",
|
|
515
|
+
"___",
|
|
516
|
+
"UPPER.Case_ID",
|
|
517
|
+
"a".repeat(200),
|
|
518
|
+
"weird@@chars!!here",
|
|
519
|
+
"123start",
|
|
520
|
+
]) {
|
|
521
|
+
const out = sanitizeAgentName(id);
|
|
522
|
+
expect(out.length).toBeGreaterThan(0);
|
|
523
|
+
expect(out.length).toBeLessThanOrEqual(63);
|
|
524
|
+
expect(out).toMatch(RFC1123);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("never exceeds 63 characters even for very long ids", () => {
|
|
529
|
+
const out = sanitizeAgentName("agent-".repeat(50));
|
|
530
|
+
expect(out.length).toBeLessThanOrEqual(63);
|
|
531
|
+
expect(out).toMatch(RFC1123);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("appends a hash suffix when the id is truncated (collision resistance)", () => {
|
|
535
|
+
const longA = `${"x".repeat(100)}aaaa`;
|
|
536
|
+
const longB = `${"x".repeat(100)}bbbb`;
|
|
537
|
+
const a = sanitizeAgentName(longA);
|
|
538
|
+
const b = sanitizeAgentName(longB);
|
|
539
|
+
// Truncation alone would collide on the shared prefix; the hash suffix
|
|
540
|
+
// derived from the full original id keeps them distinct.
|
|
541
|
+
expect(a).not.toBe(b);
|
|
542
|
+
expect(a).toMatch(RFC1123);
|
|
543
|
+
expect(b).toMatch(RFC1123);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("disambiguates ids that sanitize to the same label", () => {
|
|
547
|
+
// "agent_1" and "agent.1" both naively map to "agent-1"; the hash suffix
|
|
548
|
+
// from the distinct originals keeps the k8s names unique.
|
|
549
|
+
const a = sanitizeAgentName("agent_1");
|
|
550
|
+
const b = sanitizeAgentName("agent.1");
|
|
551
|
+
expect(a).not.toBe(b);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("produces a non-empty fallback when nothing alphanumeric remains", () => {
|
|
555
|
+
const out = sanitizeAgentName("___");
|
|
556
|
+
expect(out.length).toBeGreaterThan(0);
|
|
557
|
+
expect(out).toMatch(RFC1123);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("is deterministic for a given id", () => {
|
|
561
|
+
expect(sanitizeAgentName("agent_42")).toBe(sanitizeAgentName("agent_42"));
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// ─── task-store env injection ───────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
/** The spec the coding extension registers for task-store (a plain env spec). */
|
|
568
|
+
const TASK_STORE_SPEC = {
|
|
569
|
+
tokenEnv: "SHIPWRIGHT_TASK_STORE_TOKEN",
|
|
570
|
+
urlEnv: "SHIPWRIGHT_TASK_STORE_URL",
|
|
571
|
+
secretKey: "task-store-token",
|
|
572
|
+
url: "http://task-store.svc:4000",
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
describe("buildAgentDeploymentManifest — service integration env (task-store)", () => {
|
|
576
|
+
it("injects SHIPWRIGHT_TASK_STORE_TOKEN and SHIPWRIGHT_TASK_STORE_URL when the integration has a url", () => {
|
|
577
|
+
const d = buildAgentDeploymentManifest({
|
|
578
|
+
...deployOpts,
|
|
579
|
+
serviceIntegrations: [TASK_STORE_SPEC],
|
|
580
|
+
});
|
|
581
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
582
|
+
const byName = new Map(env.map((e) => [e.name, e]));
|
|
583
|
+
|
|
584
|
+
const tokenEnv = byName.get("SHIPWRIGHT_TASK_STORE_TOKEN");
|
|
585
|
+
expect(tokenEnv).toBeDefined();
|
|
586
|
+
expect(tokenEnv?.value).toBeUndefined();
|
|
587
|
+
expect(tokenEnv?.valueFrom?.secretKeyRef).toEqual({
|
|
588
|
+
name: deployOpts.secretName,
|
|
589
|
+
key: "task-store-token",
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const urlEnv = byName.get("SHIPWRIGHT_TASK_STORE_URL");
|
|
593
|
+
expect(urlEnv).toBeDefined();
|
|
594
|
+
expect(urlEnv?.value).toBe("http://task-store.svc:4000");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("does not inject task-store env vars when no integrations are configured", () => {
|
|
598
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
599
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
600
|
+
const names = env.map((e) => e.name);
|
|
601
|
+
expect(names).not.toContain("SHIPWRIGHT_TASK_STORE_TOKEN");
|
|
602
|
+
expect(names).not.toContain("SHIPWRIGHT_TASK_STORE_URL");
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("honours a custom integration secretKey when provided", () => {
|
|
606
|
+
const d = buildAgentDeploymentManifest({
|
|
607
|
+
...deployOpts,
|
|
608
|
+
serviceIntegrations: [{ ...TASK_STORE_SPEC, secretKey: "my-ts-token" }],
|
|
609
|
+
});
|
|
610
|
+
const env = d.spec.template.spec.containers[0].env ?? [];
|
|
611
|
+
const tokenEnv = env.find((e) => e.name === "SHIPWRIGHT_TASK_STORE_TOKEN");
|
|
612
|
+
expect(tokenEnv?.valueFrom?.secretKeyRef?.key).toBe("my-ts-token");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("places task-store env vars AFTER AGENT_HOME and BEFORE voice env vars", () => {
|
|
616
|
+
const d = buildAgentDeploymentManifest({
|
|
617
|
+
...deployOpts,
|
|
618
|
+
serviceIntegrations: [TASK_STORE_SPEC],
|
|
619
|
+
voice: { whisperServiceUrl: "http://w:9000" },
|
|
620
|
+
});
|
|
621
|
+
const names = (d.spec.template.spec.containers[0].env ?? []).map(
|
|
622
|
+
(e) => e.name,
|
|
623
|
+
);
|
|
624
|
+
const agentHomeIdx = names.indexOf("AGENT_HOME");
|
|
625
|
+
const tsTokenIdx = names.indexOf("SHIPWRIGHT_TASK_STORE_TOKEN");
|
|
626
|
+
const tsUrlIdx = names.indexOf("SHIPWRIGHT_TASK_STORE_URL");
|
|
627
|
+
const whisperIdx = names.indexOf("WHISPER_SERVICE_URL");
|
|
628
|
+
|
|
629
|
+
expect(agentHomeIdx).toBeGreaterThanOrEqual(0);
|
|
630
|
+
expect(tsTokenIdx).toBeGreaterThan(agentHomeIdx);
|
|
631
|
+
expect(tsUrlIdx).toBeGreaterThan(agentHomeIdx);
|
|
632
|
+
expect(whisperIdx).toBeGreaterThan(tsUrlIdx);
|
|
633
|
+
expect(whisperIdx).toBeGreaterThan(tsTokenIdx);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("buildAgentDeploymentManifest — claude policy", () => {
|
|
638
|
+
const claudePolicy = { configMapName: "claude-policy", hash: "abc123" };
|
|
639
|
+
|
|
640
|
+
it("adds a claude-policy configMap volume with defaultMode 292", () => {
|
|
641
|
+
const d = buildAgentDeploymentManifest({ ...deployOpts, claudePolicy });
|
|
642
|
+
const volumes = d.spec.template.spec.volumes ?? [];
|
|
643
|
+
expect(volumes).toHaveLength(2);
|
|
644
|
+
expect(volumes[1]).toEqual({
|
|
645
|
+
name: "claude-policy",
|
|
646
|
+
configMap: {
|
|
647
|
+
name: "claude-policy",
|
|
648
|
+
items: [
|
|
649
|
+
{ key: "managed-settings.json", path: "managed-settings.json" },
|
|
650
|
+
],
|
|
651
|
+
defaultMode: 292,
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it("mounts the policy directory read-only at /etc/claude-code (no subPath)", () => {
|
|
657
|
+
const d = buildAgentDeploymentManifest({ ...deployOpts, claudePolicy });
|
|
658
|
+
const mounts = d.spec.template.spec.containers[0]?.volumeMounts ?? [];
|
|
659
|
+
expect(mounts).toHaveLength(2);
|
|
660
|
+
expect(mounts[1]).toEqual({
|
|
661
|
+
name: "claude-policy",
|
|
662
|
+
mountPath: "/etc/claude-code",
|
|
663
|
+
readOnly: true,
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
it("annotates the pod template with the policy hash", () => {
|
|
668
|
+
const d = buildAgentDeploymentManifest({ ...deployOpts, claudePolicy });
|
|
669
|
+
expect(
|
|
670
|
+
d.spec.template.metadata.annotations?.[
|
|
671
|
+
"shipwright.dev/claude-policy-hash"
|
|
672
|
+
],
|
|
673
|
+
).toBe("abc123");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("omits the annotation when the hash is empty or absent", () => {
|
|
677
|
+
for (const hash of [undefined, ""]) {
|
|
678
|
+
const d = buildAgentDeploymentManifest({
|
|
679
|
+
...deployOpts,
|
|
680
|
+
claudePolicy: { configMapName: "claude-policy", hash },
|
|
681
|
+
});
|
|
682
|
+
expect(d.spec.template.metadata.annotations).toBeUndefined();
|
|
683
|
+
expect(d.spec.template.spec.volumes).toHaveLength(2);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("leaves the manifest unchanged when claudePolicy is unset", () => {
|
|
688
|
+
const d = buildAgentDeploymentManifest(deployOpts);
|
|
689
|
+
expect(d.spec.template.spec.volumes).toHaveLength(1);
|
|
690
|
+
expect(d.spec.template.spec.containers[0]?.volumeMounts).toHaveLength(1);
|
|
691
|
+
expect("annotations" in d.spec.template.metadata).toBe(false);
|
|
692
|
+
// The provisioner forwards `claudePolicy: undefined` when unset — that
|
|
693
|
+
// must build exactly the same manifest as omitting the key.
|
|
694
|
+
expect(
|
|
695
|
+
buildAgentDeploymentManifest({ ...deployOpts, claudePolicy: undefined }),
|
|
696
|
+
).toEqual(d);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// ─── Tenant pods (SSP-7.1) ──────────────────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
describe("buildAgentDeploymentManifest — tenant pods", () => {
|
|
703
|
+
it("labels Deployment + pod template and sets a CPU limit when accountId is set", () => {
|
|
704
|
+
const d = buildAgentDeploymentManifest({
|
|
705
|
+
...deployOpts,
|
|
706
|
+
accountId: "acct_1",
|
|
707
|
+
});
|
|
708
|
+
expect(d.metadata.labels?.[TENANT_LABEL]).toBe("true");
|
|
709
|
+
expect(d.spec.template.metadata.labels[TENANT_LABEL]).toBe("true");
|
|
710
|
+
// selector stays immutable-safe: no tenant label
|
|
711
|
+
expect(d.spec.selector.matchLabels[TENANT_LABEL]).toBeUndefined();
|
|
712
|
+
expect(d.spec.template.spec.containers[0]?.resources?.limits?.cpu).toBe(
|
|
713
|
+
"2",
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("honours a cpuLimit override for tenant pods", () => {
|
|
718
|
+
const d = buildAgentDeploymentManifest({
|
|
719
|
+
...deployOpts,
|
|
720
|
+
accountId: "acct_1",
|
|
721
|
+
resources: { cpuLimit: "1500m" },
|
|
722
|
+
});
|
|
723
|
+
expect(d.spec.template.spec.containers[0]?.resources?.limits?.cpu).toBe(
|
|
724
|
+
"1500m",
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("golden: no accountId (or null) is byte-identical to the platform manifest", () => {
|
|
729
|
+
const base = JSON.stringify(buildAgentDeploymentManifest(deployOpts));
|
|
730
|
+
expect(
|
|
731
|
+
JSON.stringify(
|
|
732
|
+
buildAgentDeploymentManifest({ ...deployOpts, accountId: null }),
|
|
733
|
+
),
|
|
734
|
+
).toBe(base);
|
|
735
|
+
expect(base).not.toContain("shipwright.dev/tenant");
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("never applies cpuLimit to platform agents", () => {
|
|
739
|
+
const d = buildAgentDeploymentManifest({
|
|
740
|
+
...deployOpts,
|
|
741
|
+
resources: { cpuLimit: "1" },
|
|
742
|
+
});
|
|
743
|
+
expect(
|
|
744
|
+
d.spec.template.spec.containers[0]?.resources?.limits?.cpu,
|
|
745
|
+
).toBeUndefined();
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ─── Anthropic workload identity federation ─────────────────────────────────
|
|
750
|
+
|
|
751
|
+
describe("buildAgentDeploymentManifest — anthropic federation", () => {
|
|
752
|
+
const anthropicFederation = {
|
|
753
|
+
serviceAccountName: "shipwright-agent",
|
|
754
|
+
ruleId: "fdrl_123",
|
|
755
|
+
organizationId: "org-uuid",
|
|
756
|
+
serviceAccountId: "svac_456",
|
|
757
|
+
audience: "https://api.anthropic.com",
|
|
758
|
+
expirationSeconds: 600,
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
it("runs the pod as the configured service account", () => {
|
|
762
|
+
const d = buildAgentDeploymentManifest({
|
|
763
|
+
...deployOpts,
|
|
764
|
+
anthropicFederation,
|
|
765
|
+
});
|
|
766
|
+
expect(d.spec.template.spec.serviceAccountName).toBe("shipwright-agent");
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("adds a projected anthropic-token volume with the audience and expiration", () => {
|
|
770
|
+
const d = buildAgentDeploymentManifest({
|
|
771
|
+
...deployOpts,
|
|
772
|
+
anthropicFederation: {
|
|
773
|
+
...anthropicFederation,
|
|
774
|
+
audience: "https://example.test",
|
|
775
|
+
expirationSeconds: 3600,
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
const volumes = d.spec.template.spec.volumes ?? [];
|
|
779
|
+
expect(volumes).toHaveLength(2);
|
|
780
|
+
expect(volumes[1]).toEqual({
|
|
781
|
+
name: "anthropic-token",
|
|
782
|
+
projected: {
|
|
783
|
+
sources: [
|
|
784
|
+
{
|
|
785
|
+
serviceAccountToken: {
|
|
786
|
+
audience: "https://example.test",
|
|
787
|
+
expirationSeconds: 3600,
|
|
788
|
+
path: "token",
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("mounts the token directory read-only at /var/run/secrets/anthropic.com", () => {
|
|
797
|
+
const d = buildAgentDeploymentManifest({
|
|
798
|
+
...deployOpts,
|
|
799
|
+
anthropicFederation,
|
|
800
|
+
});
|
|
801
|
+
const mounts = d.spec.template.spec.containers[0]?.volumeMounts ?? [];
|
|
802
|
+
expect(mounts).toHaveLength(2);
|
|
803
|
+
expect(mounts[1]).toEqual({
|
|
804
|
+
name: "anthropic-token",
|
|
805
|
+
mountPath: "/var/run/secrets/anthropic.com",
|
|
806
|
+
readOnly: true,
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("injects the federation env vars (no workspace id when unset)", () => {
|
|
811
|
+
const d = buildAgentDeploymentManifest({
|
|
812
|
+
...deployOpts,
|
|
813
|
+
anthropicFederation,
|
|
814
|
+
});
|
|
815
|
+
const env = d.spec.template.spec.containers[0]?.env ?? [];
|
|
816
|
+
expect(env.slice(-4)).toEqual([
|
|
817
|
+
{
|
|
818
|
+
name: "ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
819
|
+
value: "/var/run/secrets/anthropic.com/token",
|
|
820
|
+
},
|
|
821
|
+
{ name: "ANTHROPIC_FEDERATION_RULE_ID", value: "fdrl_123" },
|
|
822
|
+
{ name: "ANTHROPIC_ORGANIZATION_ID", value: "org-uuid" },
|
|
823
|
+
{ name: "ANTHROPIC_SERVICE_ACCOUNT_ID", value: "svac_456" },
|
|
824
|
+
]);
|
|
825
|
+
expect(env.some((e) => e.name === "ANTHROPIC_WORKSPACE_ID")).toBe(false);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("injects ANTHROPIC_WORKSPACE_ID when a workspace id is configured", () => {
|
|
829
|
+
const d = buildAgentDeploymentManifest({
|
|
830
|
+
...deployOpts,
|
|
831
|
+
anthropicFederation: { ...anthropicFederation, workspaceId: "wrkspc_1" },
|
|
832
|
+
});
|
|
833
|
+
const env = d.spec.template.spec.containers[0]?.env ?? [];
|
|
834
|
+
expect(env.at(-1)).toEqual({
|
|
835
|
+
name: "ANTHROPIC_WORKSPACE_ID",
|
|
836
|
+
value: "wrkspc_1",
|
|
837
|
+
});
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("composes with the claude policy volume and mount", () => {
|
|
841
|
+
const d = buildAgentDeploymentManifest({
|
|
842
|
+
...deployOpts,
|
|
843
|
+
claudePolicy: { configMapName: "claude-policy" },
|
|
844
|
+
anthropicFederation,
|
|
845
|
+
});
|
|
846
|
+
expect(d.spec.template.spec.volumes?.map((v) => v.name)).toEqual([
|
|
847
|
+
"agent-home",
|
|
848
|
+
"claude-policy",
|
|
849
|
+
"anthropic-token",
|
|
850
|
+
]);
|
|
851
|
+
expect(
|
|
852
|
+
d.spec.template.spec.containers[0]?.volumeMounts?.map((m) => m.name),
|
|
853
|
+
).toEqual(["agent-home", "claude-policy", "anthropic-token"]);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("golden: no federation config is byte-identical to the current manifest", () => {
|
|
857
|
+
const base = JSON.stringify(buildAgentDeploymentManifest(deployOpts));
|
|
858
|
+
expect(
|
|
859
|
+
JSON.stringify(
|
|
860
|
+
buildAgentDeploymentManifest({
|
|
861
|
+
...deployOpts,
|
|
862
|
+
anthropicFederation: undefined,
|
|
863
|
+
}),
|
|
864
|
+
),
|
|
865
|
+
).toBe(base);
|
|
866
|
+
expect(base).not.toContain("serviceAccountName");
|
|
867
|
+
expect(base).not.toContain("anthropic");
|
|
868
|
+
expect(base).not.toContain("ANTHROPIC_");
|
|
869
|
+
});
|
|
870
|
+
});
|