@app-vitals/agent-admin 0.0.1 → 0.1.1
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 +62 -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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/openapi-schemas.unit.test.ts
|
|
3
|
+
* Parse/reject tests for all Zod entity schemas in openapi-schemas.ts.
|
|
4
|
+
* Tests validate that good input parses cleanly and bad input produces typed errors.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { type AdminDeps, createAdminApp } from "./agents-api.ts";
|
|
9
|
+
import { type AgentRuntimeDeps, createAgentRuntimeApp } from "./api.ts";
|
|
10
|
+
import {
|
|
11
|
+
type AgentCronJob,
|
|
12
|
+
AgentCronJobSchema,
|
|
13
|
+
type AgentPlugin,
|
|
14
|
+
AgentPluginSchema,
|
|
15
|
+
type AgentToken,
|
|
16
|
+
AgentTokenSchema,
|
|
17
|
+
type AgentTool,
|
|
18
|
+
AgentToolSchema,
|
|
19
|
+
ErrorSchema,
|
|
20
|
+
} from "./openapi-schemas.ts";
|
|
21
|
+
|
|
22
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
const validCronJob = {
|
|
27
|
+
id: "cuid2",
|
|
28
|
+
agentId: "cuid1",
|
|
29
|
+
schedule: "0 9 * * 1-5",
|
|
30
|
+
prompt: "Run the morning brief",
|
|
31
|
+
channel: "C01234567",
|
|
32
|
+
user: null,
|
|
33
|
+
silent: false,
|
|
34
|
+
enabled: true,
|
|
35
|
+
preCheck: null,
|
|
36
|
+
name: "morning-brief",
|
|
37
|
+
system: false,
|
|
38
|
+
parentCronId: null,
|
|
39
|
+
lockdownDisabledAt: null,
|
|
40
|
+
createdAt: now,
|
|
41
|
+
updatedAt: now,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const validTool = {
|
|
45
|
+
id: "cuid3",
|
|
46
|
+
agentId: "cuid1",
|
|
47
|
+
pattern: "Bash",
|
|
48
|
+
enabled: true,
|
|
49
|
+
createdAt: now,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const validToken = {
|
|
53
|
+
id: "cuid4",
|
|
54
|
+
agentId: "cuid1",
|
|
55
|
+
label: "ci-token",
|
|
56
|
+
createdAt: now,
|
|
57
|
+
revokedAt: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const validPlugin = {
|
|
61
|
+
id: "cuid5",
|
|
62
|
+
agentId: "cuid1",
|
|
63
|
+
name: "shipwright",
|
|
64
|
+
version: "1.0.0",
|
|
65
|
+
enabled: true,
|
|
66
|
+
createdAt: now,
|
|
67
|
+
updatedAt: now,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ─── AgentCronJobSchema ───────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
describe("AgentCronJobSchema", () => {
|
|
73
|
+
test("parses valid cron job", () => {
|
|
74
|
+
const result = AgentCronJobSchema.safeParse(validCronJob);
|
|
75
|
+
expect(result.success).toBe(true);
|
|
76
|
+
if (result.success) {
|
|
77
|
+
const cron: AgentCronJob = result.data;
|
|
78
|
+
expect(cron.schedule).toBe("0 9 * * 1-5");
|
|
79
|
+
expect(cron.silent).toBe(false);
|
|
80
|
+
expect(cron.system).toBe(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parses cron job with user instead of channel", () => {
|
|
85
|
+
const userCron = { ...validCronJob, channel: null, user: "U01234567" };
|
|
86
|
+
const result = AgentCronJobSchema.safeParse(userCron);
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("parses cron job with all optional fields absent", () => {
|
|
91
|
+
const minimal = {
|
|
92
|
+
id: "cuid2",
|
|
93
|
+
agentId: "cuid1",
|
|
94
|
+
schedule: "0 9 * * *",
|
|
95
|
+
prompt: "Do something",
|
|
96
|
+
channel: null,
|
|
97
|
+
user: null,
|
|
98
|
+
silent: true,
|
|
99
|
+
enabled: true,
|
|
100
|
+
preCheck: null,
|
|
101
|
+
name: null,
|
|
102
|
+
system: false,
|
|
103
|
+
parentCronId: null,
|
|
104
|
+
lockdownDisabledAt: null,
|
|
105
|
+
createdAt: now,
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
};
|
|
108
|
+
const result = AgentCronJobSchema.safeParse(minimal);
|
|
109
|
+
expect(result.success).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("rejects missing schedule", () => {
|
|
113
|
+
const { schedule: _, ...noSchedule } = validCronJob;
|
|
114
|
+
const result = AgentCronJobSchema.safeParse(noSchedule);
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("rejects missing prompt", () => {
|
|
119
|
+
const { prompt: _, ...noPrompt } = validCronJob;
|
|
120
|
+
const result = AgentCronJobSchema.safeParse(noPrompt);
|
|
121
|
+
expect(result.success).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("parses cron job with a non-null parentCronId string", () => {
|
|
125
|
+
const parented = { ...validCronJob, parentCronId: "cuid-parent" };
|
|
126
|
+
const result = AgentCronJobSchema.safeParse(parented);
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
if (result.success) {
|
|
129
|
+
expect(result.data.parentCronId).toBe("cuid-parent");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("rejects missing parentCronId (required-but-nullable, matching channel/user/preCheck/name)", () => {
|
|
134
|
+
const { parentCronId: _, ...noParentCronId } = validCronJob;
|
|
135
|
+
const result = AgentCronJobSchema.safeParse(noParentCronId);
|
|
136
|
+
expect(result.success).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── AgentToolSchema ──────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("AgentToolSchema", () => {
|
|
143
|
+
test("parses valid tool", () => {
|
|
144
|
+
const result = AgentToolSchema.safeParse(validTool);
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
if (result.success) {
|
|
147
|
+
const tool: AgentTool = result.data;
|
|
148
|
+
expect(tool.pattern).toBe("Bash");
|
|
149
|
+
expect(tool.enabled).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("rejects missing pattern", () => {
|
|
154
|
+
const { pattern: _, ...noPattern } = validTool;
|
|
155
|
+
const result = AgentToolSchema.safeParse(noPattern);
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rejects missing agentId", () => {
|
|
160
|
+
const { agentId: _, ...noAgentId } = validTool;
|
|
161
|
+
const result = AgentToolSchema.safeParse(noAgentId);
|
|
162
|
+
expect(result.success).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("rejects non-boolean enabled", () => {
|
|
166
|
+
const result = AgentToolSchema.safeParse({ ...validTool, enabled: "yes" });
|
|
167
|
+
expect(result.success).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── AgentTokenSchema ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe("AgentTokenSchema", () => {
|
|
174
|
+
test("parses valid token metadata", () => {
|
|
175
|
+
const result = AgentTokenSchema.safeParse(validToken);
|
|
176
|
+
expect(result.success).toBe(true);
|
|
177
|
+
if (result.success) {
|
|
178
|
+
const token: AgentToken = result.data;
|
|
179
|
+
expect(token.label).toBe("ci-token");
|
|
180
|
+
expect(token.revokedAt).toBeNull();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("parses token without label", () => {
|
|
185
|
+
const { label: _, ...noLabel } = validToken;
|
|
186
|
+
const result = AgentTokenSchema.safeParse(noLabel);
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
if (result.success) {
|
|
189
|
+
expect(result.data.label).toBeUndefined();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("parses token with null label", () => {
|
|
194
|
+
const result = AgentTokenSchema.safeParse({ ...validToken, label: null });
|
|
195
|
+
expect(result.success).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("parses revoked token with revokedAt set", () => {
|
|
199
|
+
const revoked = { ...validToken, revokedAt: now };
|
|
200
|
+
const result = AgentTokenSchema.safeParse(revoked);
|
|
201
|
+
expect(result.success).toBe(true);
|
|
202
|
+
if (result.success) {
|
|
203
|
+
expect(result.data.revokedAt).toBe(now);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("does not expose token hash", () => {
|
|
208
|
+
const withHash = { ...validToken, token: "sha256hash" };
|
|
209
|
+
const result = AgentTokenSchema.safeParse(withHash);
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
if (result.success) {
|
|
212
|
+
expect((result.data as Record<string, unknown>).token).toBeUndefined();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("rejects missing id", () => {
|
|
217
|
+
const { id: _, ...noId } = validToken;
|
|
218
|
+
const result = AgentTokenSchema.safeParse(noId);
|
|
219
|
+
expect(result.success).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ─── AgentPluginSchema ────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe("AgentPluginSchema", () => {
|
|
226
|
+
test("parses valid plugin", () => {
|
|
227
|
+
const result = AgentPluginSchema.safeParse(validPlugin);
|
|
228
|
+
expect(result.success).toBe(true);
|
|
229
|
+
if (result.success) {
|
|
230
|
+
const plugin: AgentPlugin = result.data;
|
|
231
|
+
expect(plugin.name).toBe("shipwright");
|
|
232
|
+
expect(plugin.version).toBe("1.0.0");
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("parses plugin without version", () => {
|
|
237
|
+
const { version: _, ...noVersion } = validPlugin;
|
|
238
|
+
const result = AgentPluginSchema.safeParse(noVersion);
|
|
239
|
+
expect(result.success).toBe(true);
|
|
240
|
+
if (result.success) {
|
|
241
|
+
expect(result.data.version).toBeUndefined();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("parses plugin with null version (latest)", () => {
|
|
246
|
+
const result = AgentPluginSchema.safeParse({
|
|
247
|
+
...validPlugin,
|
|
248
|
+
version: null,
|
|
249
|
+
});
|
|
250
|
+
expect(result.success).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("rejects missing name", () => {
|
|
254
|
+
const { name: _, ...noName } = validPlugin;
|
|
255
|
+
const result = AgentPluginSchema.safeParse(noName);
|
|
256
|
+
expect(result.success).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("rejects non-boolean enabled", () => {
|
|
260
|
+
const result = AgentPluginSchema.safeParse({ ...validPlugin, enabled: 1 });
|
|
261
|
+
expect(result.success).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── ErrorSchema ─────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe("ErrorSchema", () => {
|
|
268
|
+
test("parses valid error response", () => {
|
|
269
|
+
const result = ErrorSchema.safeParse({ error: "Not found" });
|
|
270
|
+
expect(result.success).toBe(true);
|
|
271
|
+
if (result.success) {
|
|
272
|
+
expect(result.data.error).toBe("Not found");
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("rejects missing error string", () => {
|
|
277
|
+
const result = ErrorSchema.safeParse({});
|
|
278
|
+
expect(result.success).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── OpenAPI route metadata (summary/description on every createRoute) ────────
|
|
283
|
+
//
|
|
284
|
+
// Builds both OpenAPIHono apps with minimal stub deps — same pattern as
|
|
285
|
+
// scripts/generate-admin-spec.ts — and walks the generated spec's operations.
|
|
286
|
+
// No handler is ever invoked (we never call app.request()), only route
|
|
287
|
+
// registration + getOpenAPI31Document(), so the stub deps only need to satisfy
|
|
288
|
+
// the type checker, not behave correctly. Hard-cast via `unknown` rather than
|
|
289
|
+
// filling in every service method — this test asserts on route *metadata*,
|
|
290
|
+
// not handler behavior (that's covered by the smoke tests).
|
|
291
|
+
|
|
292
|
+
function collectUndocumentedOperations(
|
|
293
|
+
paths: unknown,
|
|
294
|
+
label: string,
|
|
295
|
+
): string[] {
|
|
296
|
+
const issues: string[] = [];
|
|
297
|
+
const pathEntries = Object.entries(
|
|
298
|
+
(paths ?? {}) as Record<string, Record<string, unknown>>,
|
|
299
|
+
);
|
|
300
|
+
for (const [path, methods] of pathEntries) {
|
|
301
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
302
|
+
const op = operation as { summary?: unknown; description?: unknown };
|
|
303
|
+
const summary = typeof op.summary === "string" ? op.summary.trim() : "";
|
|
304
|
+
const description =
|
|
305
|
+
typeof op.description === "string" ? op.description.trim() : "";
|
|
306
|
+
if (!summary || !description) {
|
|
307
|
+
issues.push(`[${label}] ${method.toUpperCase()} ${path}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return issues;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
describe("OpenAPI route metadata", () => {
|
|
315
|
+
test("every admin + runtime route has a non-empty summary and description", () => {
|
|
316
|
+
const runtimeApp = createAgentRuntimeApp({} as unknown as AgentRuntimeDeps);
|
|
317
|
+
const adminApp = createAdminApp({} as unknown as AdminDeps);
|
|
318
|
+
|
|
319
|
+
const runtimeDoc = runtimeApp.getOpenAPI31Document({
|
|
320
|
+
openapi: "3.1.0",
|
|
321
|
+
info: { title: "runtime (test)", version: "0.0.0" },
|
|
322
|
+
});
|
|
323
|
+
const adminDoc = adminApp.getOpenAPI31Document({
|
|
324
|
+
openapi: "3.1.0",
|
|
325
|
+
info: { title: "admin (test)", version: "0.0.0" },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const issues = [
|
|
329
|
+
...collectUndocumentedOperations(adminDoc.paths, "admin"),
|
|
330
|
+
...collectUndocumentedOperations(runtimeDoc.paths, "runtime"),
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
const totalOperations = [adminDoc, runtimeDoc].reduce(
|
|
334
|
+
(sum, doc) =>
|
|
335
|
+
sum +
|
|
336
|
+
Object.values(
|
|
337
|
+
(doc.paths ?? {}) as Record<string, Record<string, unknown>>,
|
|
338
|
+
).reduce((n, methods) => n + Object.keys(methods).length, 0),
|
|
339
|
+
0,
|
|
340
|
+
);
|
|
341
|
+
// Sanity check that this test is actually walking a non-trivial number of
|
|
342
|
+
// routes (brief cites ~38 createRoute calls across both files) — guards
|
|
343
|
+
// against the assertion silently checking zero operations.
|
|
344
|
+
expect(totalOperations).toBeGreaterThan(30);
|
|
345
|
+
|
|
346
|
+
expect(issues).toEqual([]);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/per-agent-identity.fixtures.ts
|
|
3
|
+
* Test support for per-agent identity provisioning (never imported by
|
|
4
|
+
* production code):
|
|
5
|
+
*
|
|
6
|
+
* - FakeAnthropicIdentityClient: an in-memory AnthropicIdentityClient that
|
|
7
|
+
* keeps service accounts and federation rules, enforces live-name
|
|
8
|
+
* uniqueness (409 → AnthropicConflictError) like the Admin API, and can
|
|
9
|
+
* fail any call once on demand.
|
|
10
|
+
* - InMemoryAgentIdentityStore: the AgentAnthropicIdentityService get/set
|
|
11
|
+
* contract plus the name allocator's allocate(), over a Map, with
|
|
12
|
+
* injectable save failures.
|
|
13
|
+
* - FaultyKubernetesClient: wraps a RecordedKubernetesClient so any method
|
|
14
|
+
* can fail once, and logs every call.
|
|
15
|
+
*
|
|
16
|
+
* All three append to an optional shared `events` log so tests can assert the
|
|
17
|
+
* cross-system call order.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
AgentAnthropicIdentity,
|
|
22
|
+
AgentAnthropicIdentityUpdate,
|
|
23
|
+
} from "./agent-anthropic-identity.ts";
|
|
24
|
+
import {
|
|
25
|
+
type AddServiceAccountToWorkspaceInput,
|
|
26
|
+
AnthropicApiError,
|
|
27
|
+
AnthropicConflictError,
|
|
28
|
+
type AnthropicFederationRule,
|
|
29
|
+
type AnthropicIdentityClient,
|
|
30
|
+
type AnthropicServiceAccount,
|
|
31
|
+
type AnthropicWorkspaceMember,
|
|
32
|
+
type CreateFederationRuleInput,
|
|
33
|
+
type CreateServiceAccountInput,
|
|
34
|
+
} from "./anthropic-identity-client.ts";
|
|
35
|
+
import { identityBaseName, identityName } from "./anthropic-identity-name.ts";
|
|
36
|
+
import { NotFoundError } from "./errors.ts";
|
|
37
|
+
import type {
|
|
38
|
+
KubernetesClient,
|
|
39
|
+
RecordedKubernetesClient,
|
|
40
|
+
} from "./kubernetes-client.ts";
|
|
41
|
+
|
|
42
|
+
const ARCHIVED_AT = "2026-10-01T00:00:00Z";
|
|
43
|
+
|
|
44
|
+
// ─── Anthropic identity client fake ───────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export type AnthropicIdentityOperation = keyof AnthropicIdentityClient;
|
|
47
|
+
|
|
48
|
+
export interface FakeServiceAccount extends AnthropicServiceAccount {
|
|
49
|
+
workspaces: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FakeFederationRule extends AnthropicFederationRule {
|
|
53
|
+
input: CreateFederationRuleInput;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class FakeAnthropicIdentityClient implements AnthropicIdentityClient {
|
|
57
|
+
readonly serviceAccounts = new Map<string, FakeServiceAccount>();
|
|
58
|
+
readonly rules = new Map<string, FakeFederationRule>();
|
|
59
|
+
readonly calls: Array<{ op: AnthropicIdentityOperation; arg: unknown }> = [];
|
|
60
|
+
private readonly failures = new Map<AnthropicIdentityOperation, Error>();
|
|
61
|
+
private nextId = 1;
|
|
62
|
+
|
|
63
|
+
constructor(private readonly events: string[] = []) {}
|
|
64
|
+
|
|
65
|
+
/** Make the next call to `op` throw (default: a 500 AnthropicApiError). */
|
|
66
|
+
failNext(op: AnthropicIdentityOperation, err?: Error): void {
|
|
67
|
+
this.failures.set(op, err ?? new AnthropicApiError(op, 500));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
liveServiceAccounts(): FakeServiceAccount[] {
|
|
71
|
+
return [...this.serviceAccounts.values()].filter((s) => !s.archivedAt);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
liveRules(): FakeFederationRule[] {
|
|
75
|
+
return [...this.rules.values()].filter((r) => !r.archivedAt);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
callsTo(op: AnthropicIdentityOperation): unknown[] {
|
|
79
|
+
return this.calls.filter((c) => c.op === op).map((c) => c.arg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Seed a live service account (e.g. one whose ID write was lost). */
|
|
83
|
+
seedServiceAccount(name: string): FakeServiceAccount {
|
|
84
|
+
const sa = { id: this.id("svac"), name, archivedAt: null, workspaces: [] };
|
|
85
|
+
this.serviceAccounts.set(sa.id, sa);
|
|
86
|
+
return sa;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Seed a live federation rule (e.g. one whose ID write was lost). */
|
|
90
|
+
seedRule(input: CreateFederationRuleInput): FakeFederationRule {
|
|
91
|
+
const rule = {
|
|
92
|
+
id: this.id("fdrl"),
|
|
93
|
+
name: input.name,
|
|
94
|
+
archivedAt: null,
|
|
95
|
+
input,
|
|
96
|
+
};
|
|
97
|
+
this.rules.set(rule.id, rule);
|
|
98
|
+
return rule;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async createServiceAccount(
|
|
102
|
+
input: CreateServiceAccountInput,
|
|
103
|
+
): Promise<AnthropicServiceAccount> {
|
|
104
|
+
this.enter("createServiceAccount", input);
|
|
105
|
+
if (this.liveServiceAccounts().some((s) => s.name === input.name)) {
|
|
106
|
+
throw new AnthropicConflictError("createServiceAccount");
|
|
107
|
+
}
|
|
108
|
+
const { workspaces: _w, ...sa } = this.seedServiceAccount(input.name);
|
|
109
|
+
return sa;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async addServiceAccountToWorkspace(
|
|
113
|
+
input: AddServiceAccountToWorkspaceInput,
|
|
114
|
+
): Promise<AnthropicWorkspaceMember> {
|
|
115
|
+
this.enter("addServiceAccountToWorkspace", input);
|
|
116
|
+
const sa = this.serviceAccounts.get(input.serviceAccountId);
|
|
117
|
+
if (!sa || sa.archivedAt) {
|
|
118
|
+
throw new AnthropicApiError("addServiceAccountToWorkspace", 404);
|
|
119
|
+
}
|
|
120
|
+
if (sa.workspaces.includes(input.workspaceId)) {
|
|
121
|
+
throw new AnthropicConflictError("addServiceAccountToWorkspace");
|
|
122
|
+
}
|
|
123
|
+
sa.workspaces.push(input.workspaceId);
|
|
124
|
+
return {
|
|
125
|
+
serviceAccountId: sa.id,
|
|
126
|
+
workspaceId: input.workspaceId,
|
|
127
|
+
workspaceRole: input.workspaceRole ?? "workspace_developer",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async createFederationRule(
|
|
132
|
+
input: CreateFederationRuleInput,
|
|
133
|
+
): Promise<AnthropicFederationRule> {
|
|
134
|
+
this.enter("createFederationRule", input);
|
|
135
|
+
if (this.liveRules().some((r) => r.name === input.name)) {
|
|
136
|
+
throw new AnthropicConflictError("createFederationRule");
|
|
137
|
+
}
|
|
138
|
+
const { input: _i, ...rule } = this.seedRule(input);
|
|
139
|
+
return rule;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async archiveFederationRule(
|
|
143
|
+
ruleId: string,
|
|
144
|
+
): Promise<AnthropicFederationRule> {
|
|
145
|
+
this.enter("archiveFederationRule", ruleId);
|
|
146
|
+
const rule = this.rules.get(ruleId);
|
|
147
|
+
if (!rule) throw new AnthropicApiError("archiveFederationRule", 404);
|
|
148
|
+
rule.archivedAt = ARCHIVED_AT;
|
|
149
|
+
return { id: rule.id, name: rule.name, archivedAt: rule.archivedAt };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async archiveServiceAccount(
|
|
153
|
+
serviceAccountId: string,
|
|
154
|
+
): Promise<AnthropicServiceAccount> {
|
|
155
|
+
this.enter("archiveServiceAccount", serviceAccountId);
|
|
156
|
+
const sa = this.serviceAccounts.get(serviceAccountId);
|
|
157
|
+
if (!sa) throw new AnthropicApiError("archiveServiceAccount", 404);
|
|
158
|
+
sa.archivedAt = ARCHIVED_AT;
|
|
159
|
+
return { id: sa.id, name: sa.name, archivedAt: sa.archivedAt };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async findServiceAccountByName(
|
|
163
|
+
name: string,
|
|
164
|
+
): Promise<AnthropicServiceAccount | null> {
|
|
165
|
+
this.enter("findServiceAccountByName", name);
|
|
166
|
+
const sa = this.liveServiceAccounts().find((s) => s.name === name);
|
|
167
|
+
return sa ? { id: sa.id, name: sa.name, archivedAt: null } : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async findFederationRuleByName(
|
|
171
|
+
name: string,
|
|
172
|
+
): Promise<AnthropicFederationRule | null> {
|
|
173
|
+
this.enter("findFederationRuleByName", name);
|
|
174
|
+
const rule = this.liveRules().find((r) => r.name === name);
|
|
175
|
+
return rule ? { id: rule.id, name: rule.name, archivedAt: null } : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private id(prefix: string): string {
|
|
179
|
+
return `${prefix}_fake${this.nextId++}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private enter(op: AnthropicIdentityOperation, arg: unknown): void {
|
|
183
|
+
this.calls.push({ op, arg });
|
|
184
|
+
this.events.push(`anthropic.${op}`);
|
|
185
|
+
const err = this.failures.get(op);
|
|
186
|
+
if (err) {
|
|
187
|
+
this.failures.delete(op);
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Agent identity store fake ────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const EMPTY_IDENTITY: AgentAnthropicIdentity = {
|
|
196
|
+
ownerEmail: null,
|
|
197
|
+
anthropicIdentityName: null,
|
|
198
|
+
anthropicServiceAccountId: null,
|
|
199
|
+
anthropicFederationRuleId: null,
|
|
200
|
+
k8sServiceAccountName: null,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* In-memory stand-in for AgentAnthropicIdentityService (get/set) and
|
|
205
|
+
* AnthropicIdentityNameAllocator (allocate). Names follow the real allocator's
|
|
206
|
+
* pure naming rules.
|
|
207
|
+
*/
|
|
208
|
+
export class InMemoryAgentIdentityStore {
|
|
209
|
+
readonly rows = new Map<string, AgentAnthropicIdentity>();
|
|
210
|
+
private readonly setFailures = new Map<keyof AgentAnthropicIdentity, Error>();
|
|
211
|
+
|
|
212
|
+
constructor(
|
|
213
|
+
private readonly events: string[] = [],
|
|
214
|
+
private readonly suffix?: string,
|
|
215
|
+
) {}
|
|
216
|
+
|
|
217
|
+
addAgent(
|
|
218
|
+
agentId: string,
|
|
219
|
+
identity: Partial<AgentAnthropicIdentity> = {},
|
|
220
|
+
): void {
|
|
221
|
+
this.rows.set(agentId, { ...EMPTY_IDENTITY, ...identity });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Make the next set() that writes `field` throw. */
|
|
225
|
+
failNextSetOf(field: keyof AgentAnthropicIdentity, err?: Error): void {
|
|
226
|
+
this.setFailures.set(field, err ?? new Error(`lost write of ${field}`));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async get(agentId: string): Promise<AgentAnthropicIdentity | null> {
|
|
230
|
+
const row = this.rows.get(agentId);
|
|
231
|
+
return row ? { ...row } : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async set(
|
|
235
|
+
agentId: string,
|
|
236
|
+
update: AgentAnthropicIdentityUpdate,
|
|
237
|
+
): Promise<AgentAnthropicIdentity> {
|
|
238
|
+
const fields = (
|
|
239
|
+
Object.keys(update) as (keyof AgentAnthropicIdentity)[]
|
|
240
|
+
).filter((k) => update[k] !== undefined);
|
|
241
|
+
this.events.push(`store.set(${fields.join(",")})`);
|
|
242
|
+
for (const field of fields) {
|
|
243
|
+
const err = this.setFailures.get(field);
|
|
244
|
+
if (err) {
|
|
245
|
+
this.setFailures.delete(field);
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const row = this.rows.get(agentId);
|
|
250
|
+
if (!row) throw new NotFoundError(`Agent ${agentId} not found`);
|
|
251
|
+
for (const field of fields) row[field] = update[field] ?? null;
|
|
252
|
+
return { ...row };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async allocate(agentId: string, ownerEmail: string): Promise<string> {
|
|
256
|
+
this.events.push("store.allocate");
|
|
257
|
+
const row = this.rows.get(agentId);
|
|
258
|
+
if (!row) throw new NotFoundError(`Agent ${agentId} not found`);
|
|
259
|
+
if (row.anthropicIdentityName) return row.anthropicIdentityName;
|
|
260
|
+
const base = identityBaseName(ownerEmail, this.suffix);
|
|
261
|
+
let n = 1;
|
|
262
|
+
const taken = new Set(
|
|
263
|
+
[...this.rows.values()].map((r) => r.anthropicIdentityName),
|
|
264
|
+
);
|
|
265
|
+
while (taken.has(identityName(base, n, this.suffix))) n++;
|
|
266
|
+
row.anthropicIdentityName = identityName(base, n, this.suffix);
|
|
267
|
+
return row.anthropicIdentityName;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Kubernetes client wrapper ────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A KubernetesClient that forwards to a RecordedKubernetesClient, logs each
|
|
275
|
+
* call as `k8s.<method>` and can fail any method once.
|
|
276
|
+
*/
|
|
277
|
+
export function faultyKubernetesClient(
|
|
278
|
+
inner: RecordedKubernetesClient,
|
|
279
|
+
events: string[] = [],
|
|
280
|
+
): KubernetesClient & {
|
|
281
|
+
failNext(method: keyof KubernetesClient, err?: Error): void;
|
|
282
|
+
} {
|
|
283
|
+
const failures = new Map<string, Error>();
|
|
284
|
+
const failNext = (method: keyof KubernetesClient, err?: Error) => {
|
|
285
|
+
failures.set(method, err ?? new Error(`k8s ${method} failed`));
|
|
286
|
+
};
|
|
287
|
+
return new Proxy(inner, {
|
|
288
|
+
get(target, prop, receiver) {
|
|
289
|
+
if (prop === "failNext") return failNext;
|
|
290
|
+
const value = Reflect.get(target, prop, receiver);
|
|
291
|
+
if (typeof value !== "function") return value;
|
|
292
|
+
return async (...args: unknown[]) => {
|
|
293
|
+
events.push(`k8s.${String(prop)}`);
|
|
294
|
+
const err = failures.get(String(prop));
|
|
295
|
+
if (err) {
|
|
296
|
+
failures.delete(String(prop));
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
return value.apply(target, args);
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
}) as unknown as KubernetesClient & {
|
|
303
|
+
failNext(method: keyof KubernetesClient, err?: Error): void;
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/platform-doc-route.ts
|
|
3
|
+
* Optional GET /doc route serving the platform OpenAPI 3.1 document.
|
|
4
|
+
*
|
|
5
|
+
* Mounted only when SHIPWRIGHT_ADMIN_DOC_ENABLED === "enabled"; otherwise the
|
|
6
|
+
* route is absent (Hono 404, identical to a build without this module). It
|
|
7
|
+
* serves buildPlatformAdminSpec() only — never extension (coding) paths — so
|
|
8
|
+
* the body matches the committed admin/openapi.platform.json byte-for-byte.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Hono, MiddlewareHandler } from "hono";
|
|
12
|
+
import { buildPlatformAdminSpec } from "./admin-spec.ts";
|
|
13
|
+
|
|
14
|
+
export const DOC_ENABLED_ENV = "SHIPWRIGHT_ADMIN_DOC_ENABLED";
|
|
15
|
+
|
|
16
|
+
export function isPlatformDocEnabled(
|
|
17
|
+
env: Record<string, string | undefined>,
|
|
18
|
+
): boolean {
|
|
19
|
+
return env[DOC_ENABLED_ENV] === "enabled";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Mounts GET /doc behind `auth` when the flag is on; a no-op otherwise. */
|
|
23
|
+
export function mountPlatformDoc(
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: the root app carries arbitrary Env generics
|
|
25
|
+
root: Hono<any>,
|
|
26
|
+
deps: {
|
|
27
|
+
env: Record<string, string | undefined>;
|
|
28
|
+
auth: MiddlewareHandler;
|
|
29
|
+
},
|
|
30
|
+
): void {
|
|
31
|
+
if (!isPlatformDocEnabled(deps.env)) return;
|
|
32
|
+
// Same serialization as the committed spec file (2-space JSON + newline).
|
|
33
|
+
const body = `${JSON.stringify(buildPlatformAdminSpec(), null, 2)}\n`;
|
|
34
|
+
root.get("/doc", deps.auth, (c) =>
|
|
35
|
+
c.body(body, 200, {
|
|
36
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
37
|
+
"Cache-Control": "no-store",
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|