@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,1225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/api.smoke.test.ts
|
|
3
|
+
* Smoke tests for the Hono runtime API — GET /:id/config and GET /:id/crons.
|
|
4
|
+
* Routes are registered without the /agents prefix because the sub-app is
|
|
5
|
+
* mounted via root.route("/agents", runtimeApp) — Hono v4 strips the prefix
|
|
6
|
+
* before dispatching, so root-level paths are /agents/:id/config etc.
|
|
7
|
+
* Uses injected mocks — no real DB or encryption needed.
|
|
8
|
+
*
|
|
9
|
+
* Auth: routes are now protected by the same admin-key/per-agent-token/cookie
|
|
10
|
+
* auth as the CRUD routes (SHIPWRIGHT_INTERNAL_API_KEY removed, UNI-1.2).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { Hono } from "hono";
|
|
15
|
+
import type { AgentCronJob } from "./agent-cron-jobs.ts";
|
|
16
|
+
import type { AgentEnvBundle } from "./agent-envs.ts";
|
|
17
|
+
import { NoopAgentProvisioner } from "./agent-provisioner.ts";
|
|
18
|
+
import { createAdminApp, parseAdminApiKeys } from "./agents-api.ts";
|
|
19
|
+
import { createAgentRuntimeApp } from "./api.ts";
|
|
20
|
+
|
|
21
|
+
// ─── Mock types ───────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
interface MockPlugin {
|
|
24
|
+
id: string;
|
|
25
|
+
agentId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version: string | null;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface MockAgent {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
repos: string[];
|
|
37
|
+
reviewAuthorAllowlist: string[];
|
|
38
|
+
patchAuthorAllowlist: string[];
|
|
39
|
+
restrictSlackToMembers: boolean;
|
|
40
|
+
memberEmails: string[];
|
|
41
|
+
accountStatus?: string | null;
|
|
42
|
+
selfHosted?: boolean;
|
|
43
|
+
// APM-1.5: the 6 agent-policy fields passed through by GET /:id/config.
|
|
44
|
+
autoPostReviews: boolean;
|
|
45
|
+
allowSelfReview: boolean;
|
|
46
|
+
minConfidence: number;
|
|
47
|
+
maxFindings: number;
|
|
48
|
+
cleanupMergedWorktrees: boolean;
|
|
49
|
+
cleanupAfterDays: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Mock factories ───────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function makeMockAgentEnvService(bundles: Map<string, AgentEnvBundle | null>): {
|
|
55
|
+
getConfigBundle: (id: string) => Promise<AgentEnvBundle | null>;
|
|
56
|
+
} {
|
|
57
|
+
return {
|
|
58
|
+
async getConfigBundle(id: string): Promise<AgentEnvBundle | null> {
|
|
59
|
+
if (!bundles.has(id)) return null;
|
|
60
|
+
return bundles.get(id) ?? null;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeMockAgentCronJobService(crons: Map<string, AgentCronJob[]>): {
|
|
66
|
+
list: (id: string) => Promise<AgentCronJob[]>;
|
|
67
|
+
listWithRunSummary: (
|
|
68
|
+
id: string,
|
|
69
|
+
) => Promise<(AgentCronJob & { lastRun: null; runCountToday: number })[]>;
|
|
70
|
+
} {
|
|
71
|
+
return {
|
|
72
|
+
async list(id: string): Promise<AgentCronJob[]> {
|
|
73
|
+
return crons.get(id) ?? [];
|
|
74
|
+
},
|
|
75
|
+
async listWithRunSummary(
|
|
76
|
+
id: string,
|
|
77
|
+
): Promise<(AgentCronJob & { lastRun: null; runCountToday: number })[]> {
|
|
78
|
+
const items = crons.get(id) ?? [];
|
|
79
|
+
return items.map((item) => ({
|
|
80
|
+
...item,
|
|
81
|
+
lastRun: null,
|
|
82
|
+
runCountToday: 0,
|
|
83
|
+
}));
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeMockAgentService(agents: Map<string, MockAgent>): {
|
|
89
|
+
getById: (agentId: string) => Promise<{
|
|
90
|
+
id: string;
|
|
91
|
+
repos: string[];
|
|
92
|
+
reviewAuthorAllowlist: string[];
|
|
93
|
+
patchAuthorAllowlist: string[];
|
|
94
|
+
restrictSlackToMembers: boolean;
|
|
95
|
+
memberEmails: string[];
|
|
96
|
+
accountStatus?: string | null;
|
|
97
|
+
selfHosted?: boolean;
|
|
98
|
+
autoPostReviews: boolean;
|
|
99
|
+
allowSelfReview: boolean;
|
|
100
|
+
minConfidence: number;
|
|
101
|
+
maxFindings: number;
|
|
102
|
+
cleanupMergedWorktrees: boolean;
|
|
103
|
+
cleanupAfterDays: number;
|
|
104
|
+
} | null>;
|
|
105
|
+
} {
|
|
106
|
+
return {
|
|
107
|
+
async getById(agentId: string): Promise<{
|
|
108
|
+
id: string;
|
|
109
|
+
repos: string[];
|
|
110
|
+
reviewAuthorAllowlist: string[];
|
|
111
|
+
patchAuthorAllowlist: string[];
|
|
112
|
+
restrictSlackToMembers: boolean;
|
|
113
|
+
memberEmails: string[];
|
|
114
|
+
accountStatus?: string | null;
|
|
115
|
+
selfHosted?: boolean;
|
|
116
|
+
autoPostReviews: boolean;
|
|
117
|
+
allowSelfReview: boolean;
|
|
118
|
+
minConfidence: number;
|
|
119
|
+
maxFindings: number;
|
|
120
|
+
cleanupMergedWorktrees: boolean;
|
|
121
|
+
cleanupAfterDays: number;
|
|
122
|
+
} | null> {
|
|
123
|
+
const agent = agents.get(agentId);
|
|
124
|
+
return agent
|
|
125
|
+
? {
|
|
126
|
+
id: agent.id,
|
|
127
|
+
repos: agent.repos,
|
|
128
|
+
reviewAuthorAllowlist: agent.reviewAuthorAllowlist,
|
|
129
|
+
patchAuthorAllowlist: agent.patchAuthorAllowlist,
|
|
130
|
+
restrictSlackToMembers: agent.restrictSlackToMembers,
|
|
131
|
+
memberEmails: agent.memberEmails,
|
|
132
|
+
accountStatus: agent.accountStatus,
|
|
133
|
+
selfHosted: agent.selfHosted,
|
|
134
|
+
autoPostReviews: agent.autoPostReviews,
|
|
135
|
+
allowSelfReview: agent.allowSelfReview,
|
|
136
|
+
minConfidence: agent.minConfidence,
|
|
137
|
+
maxFindings: agent.maxFindings,
|
|
138
|
+
cleanupMergedWorktrees: agent.cleanupMergedWorktrees,
|
|
139
|
+
cleanupAfterDays: agent.cleanupAfterDays,
|
|
140
|
+
}
|
|
141
|
+
: null;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function makeMockAgentPluginService(plugins: Map<string, MockPlugin[]>): {
|
|
147
|
+
listEnabled: (agentId: string) => Promise<MockPlugin[]>;
|
|
148
|
+
} {
|
|
149
|
+
return {
|
|
150
|
+
async listEnabled(agentId: string): Promise<MockPlugin[]> {
|
|
151
|
+
return (plugins.get(agentId) ?? []).filter((p) => p.enabled);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface MockPhaseMethodology {
|
|
157
|
+
phase: string;
|
|
158
|
+
subagentType: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function makeMockAgentPhaseMethodologyService(
|
|
162
|
+
overrides: Map<string, MockPhaseMethodology[]>,
|
|
163
|
+
): {
|
|
164
|
+
list: (agentId: string) => Promise<MockPhaseMethodology[]>;
|
|
165
|
+
} {
|
|
166
|
+
return {
|
|
167
|
+
async list(agentId: string): Promise<MockPhaseMethodology[]> {
|
|
168
|
+
return overrides.get(agentId) ?? [];
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Test data helpers ────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const KNOWN_AGENT_ID = "agent-123";
|
|
176
|
+
const UNKNOWN_AGENT_ID = "agent-999";
|
|
177
|
+
const VALID_ADMIN_KEY = "test-admin-key";
|
|
178
|
+
const SESSION_SECRET = "test-session-secret-32bytes!!!!";
|
|
179
|
+
|
|
180
|
+
function makeDate(offset = 0): Date {
|
|
181
|
+
return new Date(Date.now() + offset);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function makePlugin(agentId: string, name: string): MockPlugin {
|
|
185
|
+
return {
|
|
186
|
+
id: `plugin-${name}`,
|
|
187
|
+
agentId,
|
|
188
|
+
name,
|
|
189
|
+
version: null,
|
|
190
|
+
enabled: true,
|
|
191
|
+
createdAt: makeDate(),
|
|
192
|
+
updatedAt: makeDate(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makeCron(agentId: string, id: string): AgentCronJob {
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
agentId,
|
|
200
|
+
schedule: "0 9 * * *",
|
|
201
|
+
prompt: "Good morning",
|
|
202
|
+
channel: "C123",
|
|
203
|
+
user: null,
|
|
204
|
+
silent: false,
|
|
205
|
+
enabled: true,
|
|
206
|
+
preCheck: null,
|
|
207
|
+
name: null,
|
|
208
|
+
system: false,
|
|
209
|
+
parentCronId: null,
|
|
210
|
+
lockdownDisabledAt: null,
|
|
211
|
+
createdAt: makeDate(),
|
|
212
|
+
updatedAt: makeDate(),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Shared setup ─────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function buildApp(opts?: {
|
|
219
|
+
hasAgent?: boolean;
|
|
220
|
+
bundleOrNull?: AgentEnvBundle | null;
|
|
221
|
+
plugins?: MockPlugin[];
|
|
222
|
+
crons?: AgentCronJob[];
|
|
223
|
+
repos?: string[];
|
|
224
|
+
reviewAuthorAllowlist?: string[];
|
|
225
|
+
patchAuthorAllowlist?: string[];
|
|
226
|
+
restrictSlackToMembers?: boolean;
|
|
227
|
+
memberEmails?: string[];
|
|
228
|
+
accountStatus?: string | null;
|
|
229
|
+
selfHosted?: boolean;
|
|
230
|
+
anthropicFederationEnabled?: boolean;
|
|
231
|
+
phaseMethodology?: MockPhaseMethodology[];
|
|
232
|
+
omitPhaseMethodologyService?: boolean;
|
|
233
|
+
autoPostReviews?: boolean;
|
|
234
|
+
allowSelfReview?: boolean;
|
|
235
|
+
minConfidence?: number;
|
|
236
|
+
maxFindings?: number;
|
|
237
|
+
cleanupMergedWorktrees?: boolean;
|
|
238
|
+
cleanupAfterDays?: number;
|
|
239
|
+
}) {
|
|
240
|
+
const hasAgent = opts?.hasAgent ?? true;
|
|
241
|
+
const bundle: AgentEnvBundle | null =
|
|
242
|
+
opts?.bundleOrNull !== undefined
|
|
243
|
+
? opts.bundleOrNull
|
|
244
|
+
: {
|
|
245
|
+
agentId: KNOWN_AGENT_ID,
|
|
246
|
+
env: { SLACK_BOT_TOKEN: "xoxb-secret", ANTHROPIC_API_KEY: "sk-ant" },
|
|
247
|
+
allowedTools: ["Read", "Write", "Bash"],
|
|
248
|
+
};
|
|
249
|
+
const plugins = opts?.plugins ?? [
|
|
250
|
+
makePlugin(KNOWN_AGENT_ID, "shipwright@shipwright"),
|
|
251
|
+
];
|
|
252
|
+
const crons = opts?.crons ?? [makeCron(KNOWN_AGENT_ID, "cron-1")];
|
|
253
|
+
const repos = opts?.repos ?? ["org/repo1", "org/repo2"];
|
|
254
|
+
const reviewAuthorAllowlist = opts?.reviewAuthorAllowlist ?? [];
|
|
255
|
+
const patchAuthorAllowlist = opts?.patchAuthorAllowlist ?? [];
|
|
256
|
+
const restrictSlackToMembers = opts?.restrictSlackToMembers ?? false;
|
|
257
|
+
const memberEmails = opts?.memberEmails ?? [];
|
|
258
|
+
const phaseMethodology = opts?.phaseMethodology ?? [];
|
|
259
|
+
const autoPostReviews = opts?.autoPostReviews ?? true;
|
|
260
|
+
const allowSelfReview = opts?.allowSelfReview ?? false;
|
|
261
|
+
const minConfidence = opts?.minConfidence ?? 75;
|
|
262
|
+
const maxFindings = opts?.maxFindings ?? 5;
|
|
263
|
+
const cleanupMergedWorktrees = opts?.cleanupMergedWorktrees ?? true;
|
|
264
|
+
const cleanupAfterDays = opts?.cleanupAfterDays ?? 14;
|
|
265
|
+
|
|
266
|
+
const agents = new Map<string, MockAgent>();
|
|
267
|
+
const bundles = new Map<string, AgentEnvBundle | null>();
|
|
268
|
+
const pluginMap = new Map<string, MockPlugin[]>();
|
|
269
|
+
const cronMap = new Map<string, AgentCronJob[]>();
|
|
270
|
+
const phaseMethodologyMap = new Map<string, MockPhaseMethodology[]>();
|
|
271
|
+
|
|
272
|
+
if (hasAgent) {
|
|
273
|
+
agents.set(KNOWN_AGENT_ID, {
|
|
274
|
+
id: KNOWN_AGENT_ID,
|
|
275
|
+
name: "Test Agent",
|
|
276
|
+
repos,
|
|
277
|
+
reviewAuthorAllowlist,
|
|
278
|
+
patchAuthorAllowlist,
|
|
279
|
+
restrictSlackToMembers,
|
|
280
|
+
memberEmails,
|
|
281
|
+
accountStatus: opts?.accountStatus,
|
|
282
|
+
selfHosted: opts?.selfHosted,
|
|
283
|
+
autoPostReviews,
|
|
284
|
+
allowSelfReview,
|
|
285
|
+
minConfidence,
|
|
286
|
+
maxFindings,
|
|
287
|
+
cleanupMergedWorktrees,
|
|
288
|
+
cleanupAfterDays,
|
|
289
|
+
});
|
|
290
|
+
bundles.set(KNOWN_AGENT_ID, bundle);
|
|
291
|
+
pluginMap.set(KNOWN_AGENT_ID, plugins);
|
|
292
|
+
cronMap.set(KNOWN_AGENT_ID, crons);
|
|
293
|
+
phaseMethodologyMap.set(KNOWN_AGENT_ID, phaseMethodology);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return createAgentRuntimeApp({
|
|
297
|
+
agentEnvService: makeMockAgentEnvService(bundles),
|
|
298
|
+
agentCronJobService: makeMockAgentCronJobService(cronMap),
|
|
299
|
+
agentService: makeMockAgentService(agents),
|
|
300
|
+
agentPluginService: makeMockAgentPluginService(pluginMap),
|
|
301
|
+
...(opts?.omitPhaseMethodologyService
|
|
302
|
+
? {}
|
|
303
|
+
: {
|
|
304
|
+
agentPhaseMethodologyService:
|
|
305
|
+
makeMockAgentPhaseMethodologyService(phaseMethodologyMap),
|
|
306
|
+
}),
|
|
307
|
+
adminApiKeys: parseAdminApiKeys(`admin:${VALID_ADMIN_KEY}:*`),
|
|
308
|
+
agentTokenService: { validate: async () => null },
|
|
309
|
+
sessionSecret: SESSION_SECRET,
|
|
310
|
+
anthropicFederationEnabled: opts?.anthropicFederationEnabled,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
describe("GET /:id/config with no phase-methodology service (platform only)", () => {
|
|
317
|
+
test("200 and omits phaseMethodology", async () => {
|
|
318
|
+
const app = buildApp({ omitPhaseMethodologyService: true });
|
|
319
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
320
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
321
|
+
});
|
|
322
|
+
expect(res.status).toBe(200);
|
|
323
|
+
const body = await res.json();
|
|
324
|
+
expect("phaseMethodology" in body).toBe(false);
|
|
325
|
+
expect(body.repos).toEqual(["org/repo1", "org/repo2"]);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("GET /:id/config (mounted as GET /agents/:id/config from root)", () => {
|
|
330
|
+
test("200 with full bundle when agent exists with env vars, tools, plugins", async () => {
|
|
331
|
+
const app = buildApp();
|
|
332
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
333
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(res.status).toBe(200);
|
|
337
|
+
const body = await res.json();
|
|
338
|
+
expect(body.env).toEqual({
|
|
339
|
+
SLACK_BOT_TOKEN: "xoxb-secret",
|
|
340
|
+
ANTHROPIC_API_KEY: "sk-ant",
|
|
341
|
+
});
|
|
342
|
+
expect(body.allowedTools).toEqual(["Read", "Write", "Bash"]);
|
|
343
|
+
expect(body.plugins).toEqual([
|
|
344
|
+
{ marketplace: "shipwright", plugin: "shipwright" },
|
|
345
|
+
]);
|
|
346
|
+
expect(body.repos).toEqual(["org/repo1", "org/repo2"]);
|
|
347
|
+
expect(body.phaseMethodology).toEqual({
|
|
348
|
+
prd: null,
|
|
349
|
+
"plan-session": null,
|
|
350
|
+
review: null,
|
|
351
|
+
patch: null,
|
|
352
|
+
deploy: null,
|
|
353
|
+
"dev-task": null,
|
|
354
|
+
});
|
|
355
|
+
// APM-1.5: additive fields, default values — no behavior change for
|
|
356
|
+
// existing consumers of this route.
|
|
357
|
+
expect(body.autoPostReviews).toBe(true);
|
|
358
|
+
expect(body.allowSelfReview).toBe(false);
|
|
359
|
+
expect(body.minConfidence).toBe(75);
|
|
360
|
+
expect(body.maxFindings).toBe(5);
|
|
361
|
+
expect(body.cleanupMergedWorktrees).toBe(true);
|
|
362
|
+
expect(body.cleanupAfterDays).toBe(14);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("200 returns the 6 agent-policy fields exactly as stored on the agent (APM-1.5)", async () => {
|
|
366
|
+
const app = buildApp({
|
|
367
|
+
autoPostReviews: false,
|
|
368
|
+
allowSelfReview: true,
|
|
369
|
+
minConfidence: 90,
|
|
370
|
+
maxFindings: 12,
|
|
371
|
+
cleanupMergedWorktrees: false,
|
|
372
|
+
cleanupAfterDays: 30,
|
|
373
|
+
});
|
|
374
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
375
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(res.status).toBe(200);
|
|
379
|
+
const body = await res.json();
|
|
380
|
+
expect(body.autoPostReviews).toBe(false);
|
|
381
|
+
expect(body.allowSelfReview).toBe(true);
|
|
382
|
+
expect(body.minConfidence).toBe(90);
|
|
383
|
+
expect(body.maxFindings).toBe(12);
|
|
384
|
+
expect(body.cleanupMergedWorktrees).toBe(false);
|
|
385
|
+
expect(body.cleanupAfterDays).toBe(30);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("200 returns accountStatus null when the agent has no account", async () => {
|
|
389
|
+
const app = buildApp({});
|
|
390
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
391
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
392
|
+
});
|
|
393
|
+
expect(res.status).toBe(200);
|
|
394
|
+
expect((await res.json()).accountStatus).toBeNull();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("200 returns accountStatus exactly as stored on the owning account", async () => {
|
|
398
|
+
const app = buildApp({ accountStatus: "suspended" });
|
|
399
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
400
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
401
|
+
});
|
|
402
|
+
expect(res.status).toBe(200);
|
|
403
|
+
expect((await res.json()).accountStatus).toBe("suspended");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("200 phaseMethodology defaults all six phases to null when the agent has no overrides", async () => {
|
|
407
|
+
const app = buildApp({ phaseMethodology: [] });
|
|
408
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
409
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(res.status).toBe(200);
|
|
413
|
+
const body = await res.json();
|
|
414
|
+
expect(body.phaseMethodology).toEqual({
|
|
415
|
+
prd: null,
|
|
416
|
+
"plan-session": null,
|
|
417
|
+
review: null,
|
|
418
|
+
patch: null,
|
|
419
|
+
deploy: null,
|
|
420
|
+
"dev-task": null,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("200 phaseMethodology reflects an override while leaving other phases null", async () => {
|
|
425
|
+
const app = buildApp({
|
|
426
|
+
phaseMethodology: [
|
|
427
|
+
{ phase: "review", subagentType: "shipwright:code-reviewer" },
|
|
428
|
+
],
|
|
429
|
+
});
|
|
430
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
431
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(res.status).toBe(200);
|
|
435
|
+
const body = await res.json();
|
|
436
|
+
expect(body.phaseMethodology).toEqual({
|
|
437
|
+
prd: null,
|
|
438
|
+
"plan-session": null,
|
|
439
|
+
review: "shipwright:code-reviewer",
|
|
440
|
+
patch: null,
|
|
441
|
+
deploy: null,
|
|
442
|
+
"dev-task": null,
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("200 returns repos exactly as stored on the agent, including empty", async () => {
|
|
447
|
+
const app = buildApp({ repos: [] });
|
|
448
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
449
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(res.status).toBe(200);
|
|
453
|
+
const body = await res.json();
|
|
454
|
+
expect(body.repos).toEqual([]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("200 returns reviewAuthorAllowlist exactly as stored on the agent", async () => {
|
|
458
|
+
const app = buildApp({ reviewAuthorAllowlist: ["octocat", "hubot"] });
|
|
459
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
460
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(res.status).toBe(200);
|
|
464
|
+
const body = await res.json();
|
|
465
|
+
expect(body.reviewAuthorAllowlist).toEqual(["octocat", "hubot"]);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("200 returns reviewAuthorAllowlist as an empty array when the agent has none", async () => {
|
|
469
|
+
const app = buildApp({ reviewAuthorAllowlist: [] });
|
|
470
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
471
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(res.status).toBe(200);
|
|
475
|
+
const body = await res.json();
|
|
476
|
+
expect(body.reviewAuthorAllowlist).toEqual([]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// DBR-2.4 regression: the legacy field must be gone for good.
|
|
480
|
+
test("200 config bundle does not include the removed authorAllowlist field", async () => {
|
|
481
|
+
const app = buildApp({ reviewAuthorAllowlist: ["octocat"] });
|
|
482
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
483
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(res.status).toBe(200);
|
|
487
|
+
const body = await res.json();
|
|
488
|
+
expect(body).not.toHaveProperty("authorAllowlist");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("200 returns restrictSlackToMembers and memberEmails exactly as stored on the agent", async () => {
|
|
492
|
+
const app = buildApp({
|
|
493
|
+
restrictSlackToMembers: true,
|
|
494
|
+
memberEmails: ["dev@example.com", "ops@example.com"],
|
|
495
|
+
});
|
|
496
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
497
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(res.status).toBe(200);
|
|
501
|
+
const body = await res.json();
|
|
502
|
+
expect(body.restrictSlackToMembers).toBe(true);
|
|
503
|
+
expect(body.memberEmails).toEqual(["dev@example.com", "ops@example.com"]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("200 returns restrictSlackToMembers false and memberEmails empty when the agent has no members", async () => {
|
|
507
|
+
const app = buildApp({ restrictSlackToMembers: false, memberEmails: [] });
|
|
508
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
509
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
expect(res.status).toBe(200);
|
|
513
|
+
const body = await res.json();
|
|
514
|
+
expect(body.restrictSlackToMembers).toBe(false);
|
|
515
|
+
expect(body.memberEmails).toEqual([]);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("parses the canonical plugin@marketplace spec, defaulting bare names to shipwright", async () => {
|
|
519
|
+
const app = buildApp({
|
|
520
|
+
plugins: [
|
|
521
|
+
makePlugin(KNOWN_AGENT_ID, "time-tracker@acme"),
|
|
522
|
+
makePlugin(KNOWN_AGENT_ID, "shipwright@shipwright"),
|
|
523
|
+
makePlugin(KNOWN_AGENT_ID, "unscoped-plugin"),
|
|
524
|
+
makePlugin(KNOWN_AGENT_ID, "my-plugin@org/my-marketplace"),
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
528
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(res.status).toBe(200);
|
|
532
|
+
const body = await res.json();
|
|
533
|
+
expect(body.plugins).toEqual([
|
|
534
|
+
{ marketplace: "acme", plugin: "time-tracker" },
|
|
535
|
+
{ marketplace: "shipwright", plugin: "shipwright" },
|
|
536
|
+
{ marketplace: "shipwright", plugin: "unscoped-plugin" },
|
|
537
|
+
// Splits on the first "@" so a marketplace can itself contain a "/".
|
|
538
|
+
{ marketplace: "org/my-marketplace", plugin: "my-plugin" },
|
|
539
|
+
]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("200 with empty env and tools when agent has no env vars set", async () => {
|
|
543
|
+
const app = buildApp({ bundleOrNull: null });
|
|
544
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
545
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
expect(res.status).toBe(200);
|
|
549
|
+
const body = await res.json();
|
|
550
|
+
expect(body.env).toEqual({});
|
|
551
|
+
expect(body.allowedTools).toEqual([]);
|
|
552
|
+
// plugins still returned (the agent has them)
|
|
553
|
+
expect(body.plugins).toEqual([
|
|
554
|
+
{ marketplace: "shipwright", plugin: "shipwright" },
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("401 when no Authorization header", async () => {
|
|
559
|
+
const app = buildApp();
|
|
560
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`);
|
|
561
|
+
|
|
562
|
+
expect(res.status).toBe(401);
|
|
563
|
+
const body = await res.json();
|
|
564
|
+
expect(body.error).toBe("Unauthorized");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("401 when wrong API key", async () => {
|
|
568
|
+
const app = buildApp();
|
|
569
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
570
|
+
headers: { Authorization: "Bearer wrong-key" },
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
expect(res.status).toBe(401);
|
|
574
|
+
const body = await res.json();
|
|
575
|
+
expect(body.error).toBe("Unauthorized");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("404 for unknown agent ID", async () => {
|
|
579
|
+
const app = buildApp();
|
|
580
|
+
const res = await app.request(`/${UNKNOWN_AGENT_ID}/config`, {
|
|
581
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(res.status).toBe(404);
|
|
585
|
+
const body = await res.json();
|
|
586
|
+
expect(body.error).toBe("Not found");
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe("GET /:id/crons (mounted as GET /agents/:id/crons from root)", () => {
|
|
591
|
+
test("200 with cron list for known agent", async () => {
|
|
592
|
+
const app = buildApp();
|
|
593
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/crons`, {
|
|
594
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
expect(res.status).toBe(200);
|
|
598
|
+
const body = await res.json();
|
|
599
|
+
expect(Array.isArray(body)).toBe(true);
|
|
600
|
+
expect(body).toHaveLength(1);
|
|
601
|
+
expect(body[0].id).toBe("cron-1");
|
|
602
|
+
expect(body[0].agentId).toBe(KNOWN_AGENT_ID);
|
|
603
|
+
expect(body[0].schedule).toBe("0 9 * * *");
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("401 when no Authorization header", async () => {
|
|
607
|
+
const app = buildApp();
|
|
608
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/crons`);
|
|
609
|
+
|
|
610
|
+
expect(res.status).toBe(401);
|
|
611
|
+
const body = await res.json();
|
|
612
|
+
expect(body.error).toBe("Unauthorized");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("401 when wrong API key", async () => {
|
|
616
|
+
const app = buildApp();
|
|
617
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/crons`, {
|
|
618
|
+
headers: { Authorization: "Bearer wrong-key" },
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
expect(res.status).toBe(401);
|
|
622
|
+
const body = await res.json();
|
|
623
|
+
expect(body.error).toBe("Unauthorized");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("404 for unknown agent ID", async () => {
|
|
627
|
+
const app = buildApp();
|
|
628
|
+
const res = await app.request(`/${UNKNOWN_AGENT_ID}/crons`, {
|
|
629
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
expect(res.status).toBe(404);
|
|
633
|
+
const body = await res.json();
|
|
634
|
+
expect(body.error).toBe("Not found");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("response includes parentCronId — null for a top-level cron, set for a parented one (this is the real payload syncCrons() consumes)", async () => {
|
|
638
|
+
const topLevel = makeCron(KNOWN_AGENT_ID, "cron-top");
|
|
639
|
+
const parented = {
|
|
640
|
+
...makeCron(KNOWN_AGENT_ID, "cron-child"),
|
|
641
|
+
name: "shipwright-dev-task",
|
|
642
|
+
parentCronId: "cron-top",
|
|
643
|
+
};
|
|
644
|
+
const app = buildApp({ crons: [topLevel, parented] });
|
|
645
|
+
const res = await app.request(`/${KNOWN_AGENT_ID}/crons`, {
|
|
646
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(res.status).toBe(200);
|
|
650
|
+
const body = await res.json();
|
|
651
|
+
const top = body.find((c: { id: string }) => c.id === "cron-top");
|
|
652
|
+
const child = body.find((c: { id: string }) => c.id === "cron-child");
|
|
653
|
+
expect(top.parentCronId).toBeNull();
|
|
654
|
+
expect(child.parentCronId).toBe("cron-top");
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ─── Combined-server mount tests ──────────────────────────────────────────────
|
|
659
|
+
//
|
|
660
|
+
// Verifies that the runtimeApp's internal key middleware does NOT intercept
|
|
661
|
+
// admin CRUD requests when both apps are mounted on root as in main.ts.
|
|
662
|
+
// This is the regression guard for the auth middleware defect fixed in this PR:
|
|
663
|
+
// root.route("/agents", runtimeApp) + root.route("/", adminApiApp)
|
|
664
|
+
// Before the fix, runtimeApp.use("*") was hoisted as a /agents/* guard in root,
|
|
665
|
+
// causing all admin CRUD requests to 401 before reaching adminApiApp handlers.
|
|
666
|
+
|
|
667
|
+
const COMBINED_ADMIN_KEY = "combined-test-admin-key";
|
|
668
|
+
const COMBINED_SESSION_SECRET = "combined-test-session-secret-32b!";
|
|
669
|
+
const COMBINED_AGENT_ID = "combined-agent-123";
|
|
670
|
+
|
|
671
|
+
function buildCombinedApp() {
|
|
672
|
+
const runtimeApp = createAgentRuntimeApp({
|
|
673
|
+
agentEnvService: {
|
|
674
|
+
async getConfigBundle() {
|
|
675
|
+
return { agentId: COMBINED_AGENT_ID, env: {}, allowedTools: [] };
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
agentCronJobService: {
|
|
679
|
+
async list() {
|
|
680
|
+
return [];
|
|
681
|
+
},
|
|
682
|
+
async listWithRunSummary() {
|
|
683
|
+
return [];
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
agentService: {
|
|
687
|
+
async getById() {
|
|
688
|
+
return {
|
|
689
|
+
id: COMBINED_AGENT_ID,
|
|
690
|
+
repos: [],
|
|
691
|
+
reviewAuthorAllowlist: [],
|
|
692
|
+
patchAuthorAllowlist: [],
|
|
693
|
+
restrictSlackToMembers: false,
|
|
694
|
+
memberEmails: [],
|
|
695
|
+
// APM-1.5: required by AgentConfigResponseSchema.
|
|
696
|
+
autoPostReviews: true,
|
|
697
|
+
allowSelfReview: false,
|
|
698
|
+
minConfidence: 75,
|
|
699
|
+
maxFindings: 5,
|
|
700
|
+
cleanupMergedWorktrees: true,
|
|
701
|
+
cleanupAfterDays: 14,
|
|
702
|
+
};
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
agentPluginService: {
|
|
706
|
+
async listEnabled() {
|
|
707
|
+
return [];
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
agentPhaseMethodologyService: {
|
|
711
|
+
async list() {
|
|
712
|
+
return [];
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
adminApiKeys: parseAdminApiKeys(`admin:${COMBINED_ADMIN_KEY}:*`),
|
|
716
|
+
agentTokenService: { validate: async () => null },
|
|
717
|
+
sessionSecret: COMBINED_SESSION_SECRET,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const adminApp = createAdminApp({
|
|
721
|
+
agentService: {
|
|
722
|
+
create: async () => ({
|
|
723
|
+
id: "a1",
|
|
724
|
+
name: "",
|
|
725
|
+
slackId: null,
|
|
726
|
+
selfHosted: false,
|
|
727
|
+
repos: [],
|
|
728
|
+
reviewAuthorAllowlist: [],
|
|
729
|
+
patchAuthorAllowlist: [],
|
|
730
|
+
restrictSlackToMembers: false,
|
|
731
|
+
typeName: "coding",
|
|
732
|
+
createdAt: new Date(),
|
|
733
|
+
updatedAt: new Date(),
|
|
734
|
+
missingRequiredEnv: [],
|
|
735
|
+
}),
|
|
736
|
+
delete: async () => {},
|
|
737
|
+
list: async () => [],
|
|
738
|
+
getSummary: async () => null,
|
|
739
|
+
getDetail: async () => null,
|
|
740
|
+
exists: async () => false,
|
|
741
|
+
updateSelfHosted: async () => ({
|
|
742
|
+
id: "a1",
|
|
743
|
+
name: "",
|
|
744
|
+
slackId: null,
|
|
745
|
+
selfHosted: false,
|
|
746
|
+
repos: [],
|
|
747
|
+
reviewAuthorAllowlist: [],
|
|
748
|
+
patchAuthorAllowlist: [],
|
|
749
|
+
restrictSlackToMembers: false,
|
|
750
|
+
typeName: "coding",
|
|
751
|
+
createdAt: new Date(),
|
|
752
|
+
updatedAt: new Date(),
|
|
753
|
+
missingRequiredEnv: [],
|
|
754
|
+
}),
|
|
755
|
+
updateFields: async () => {
|
|
756
|
+
throw new Error("not implemented");
|
|
757
|
+
},
|
|
758
|
+
runTransaction: async (fn) => fn(undefined as never),
|
|
759
|
+
},
|
|
760
|
+
specialtyRegistry: {
|
|
761
|
+
getManifest: () => {
|
|
762
|
+
throw new Error("not implemented");
|
|
763
|
+
},
|
|
764
|
+
tryGetManifest: () => undefined,
|
|
765
|
+
listTypes: () => [],
|
|
766
|
+
},
|
|
767
|
+
agentEnvService: {
|
|
768
|
+
upsert: async () => {},
|
|
769
|
+
patch: async () => {},
|
|
770
|
+
getByAgentId: async () => ({ env: {}, secretKeys: [] }),
|
|
771
|
+
deleteKey: async () => {},
|
|
772
|
+
},
|
|
773
|
+
agentCronJobService: {
|
|
774
|
+
list: async () => [],
|
|
775
|
+
listWithRunSummary: async () => [],
|
|
776
|
+
create: async () => ({
|
|
777
|
+
id: "c1",
|
|
778
|
+
agentId: COMBINED_AGENT_ID,
|
|
779
|
+
schedule: "",
|
|
780
|
+
prompt: "",
|
|
781
|
+
channel: null,
|
|
782
|
+
user: null,
|
|
783
|
+
silent: false,
|
|
784
|
+
enabled: true,
|
|
785
|
+
preCheck: null,
|
|
786
|
+
name: null,
|
|
787
|
+
system: false,
|
|
788
|
+
parentCronId: null,
|
|
789
|
+
lockdownDisabledAt: null,
|
|
790
|
+
createdAt: new Date(),
|
|
791
|
+
updatedAt: new Date(),
|
|
792
|
+
}),
|
|
793
|
+
update: async () => ({
|
|
794
|
+
id: "c1",
|
|
795
|
+
agentId: COMBINED_AGENT_ID,
|
|
796
|
+
schedule: "",
|
|
797
|
+
prompt: "",
|
|
798
|
+
channel: null,
|
|
799
|
+
user: null,
|
|
800
|
+
silent: false,
|
|
801
|
+
enabled: true,
|
|
802
|
+
preCheck: null,
|
|
803
|
+
name: null,
|
|
804
|
+
system: false,
|
|
805
|
+
parentCronId: null,
|
|
806
|
+
lockdownDisabledAt: null,
|
|
807
|
+
createdAt: new Date(),
|
|
808
|
+
updatedAt: new Date(),
|
|
809
|
+
}),
|
|
810
|
+
delete: async () => {},
|
|
811
|
+
setEnabled: async (_agentId, _cronId, enabled) => ({
|
|
812
|
+
id: "c1",
|
|
813
|
+
agentId: COMBINED_AGENT_ID,
|
|
814
|
+
schedule: "",
|
|
815
|
+
prompt: "",
|
|
816
|
+
channel: null,
|
|
817
|
+
user: null,
|
|
818
|
+
silent: false,
|
|
819
|
+
enabled,
|
|
820
|
+
preCheck: null,
|
|
821
|
+
name: null,
|
|
822
|
+
system: false,
|
|
823
|
+
parentCronId: null,
|
|
824
|
+
lockdownDisabledAt: null,
|
|
825
|
+
createdAt: new Date(),
|
|
826
|
+
updatedAt: new Date(),
|
|
827
|
+
}),
|
|
828
|
+
updatePreCheck: async (_agentId, _cronId, preCheck) => ({
|
|
829
|
+
id: "c1",
|
|
830
|
+
agentId: COMBINED_AGENT_ID,
|
|
831
|
+
schedule: "",
|
|
832
|
+
prompt: "",
|
|
833
|
+
channel: null,
|
|
834
|
+
user: null,
|
|
835
|
+
silent: false,
|
|
836
|
+
enabled: true,
|
|
837
|
+
preCheck,
|
|
838
|
+
name: null,
|
|
839
|
+
system: false,
|
|
840
|
+
parentCronId: null,
|
|
841
|
+
lockdownDisabledAt: null,
|
|
842
|
+
createdAt: new Date(),
|
|
843
|
+
updatedAt: new Date(),
|
|
844
|
+
}),
|
|
845
|
+
reconcileSystemCrons: async () => ({
|
|
846
|
+
created: 0,
|
|
847
|
+
updated: 0,
|
|
848
|
+
deleted: 0,
|
|
849
|
+
}),
|
|
850
|
+
get: async () => ({
|
|
851
|
+
id: "c1",
|
|
852
|
+
agentId: COMBINED_AGENT_ID,
|
|
853
|
+
schedule: "",
|
|
854
|
+
prompt: "",
|
|
855
|
+
channel: null,
|
|
856
|
+
user: null,
|
|
857
|
+
silent: false,
|
|
858
|
+
enabled: true,
|
|
859
|
+
preCheck: null,
|
|
860
|
+
name: null,
|
|
861
|
+
system: false,
|
|
862
|
+
parentCronId: null,
|
|
863
|
+
lockdownDisabledAt: null,
|
|
864
|
+
createdAt: new Date(),
|
|
865
|
+
updatedAt: new Date(),
|
|
866
|
+
}),
|
|
867
|
+
},
|
|
868
|
+
agentCronRunService: {
|
|
869
|
+
create: async () => ({
|
|
870
|
+
id: "run-1",
|
|
871
|
+
cronId: "c1",
|
|
872
|
+
agentId: COMBINED_AGENT_ID,
|
|
873
|
+
startedAt: new Date(),
|
|
874
|
+
completedAt: null,
|
|
875
|
+
skipped: false,
|
|
876
|
+
skipReason: null,
|
|
877
|
+
outcome: null,
|
|
878
|
+
error: null,
|
|
879
|
+
phase: null,
|
|
880
|
+
itemType: null,
|
|
881
|
+
itemId: null,
|
|
882
|
+
commandVariant: null,
|
|
883
|
+
sessionId: null,
|
|
884
|
+
lastHeartbeatAt: null,
|
|
885
|
+
baselineModel: null,
|
|
886
|
+
baselineContextTokens: null,
|
|
887
|
+
baselineInputTokens: null,
|
|
888
|
+
baselineCacheCreationTokens: null,
|
|
889
|
+
baselineCacheReadTokens: null,
|
|
890
|
+
turns: null,
|
|
891
|
+
toolCalls: null,
|
|
892
|
+
contextFingerprint: null,
|
|
893
|
+
pluginVersion: null,
|
|
894
|
+
claudeCodeVersion: null,
|
|
895
|
+
phaseId: null,
|
|
896
|
+
inputTokens: null,
|
|
897
|
+
outputTokens: null,
|
|
898
|
+
cacheReadTokens: null,
|
|
899
|
+
cacheCreationTokens: null,
|
|
900
|
+
costUsd: null,
|
|
901
|
+
model: null,
|
|
902
|
+
createdAt: new Date(),
|
|
903
|
+
}),
|
|
904
|
+
list: async () => ({
|
|
905
|
+
items: [],
|
|
906
|
+
total: 0,
|
|
907
|
+
limit: 20,
|
|
908
|
+
offset: 0,
|
|
909
|
+
}),
|
|
910
|
+
patch: async () => ({
|
|
911
|
+
id: "run-1",
|
|
912
|
+
cronId: "c1",
|
|
913
|
+
agentId: COMBINED_AGENT_ID,
|
|
914
|
+
startedAt: new Date(),
|
|
915
|
+
completedAt: null,
|
|
916
|
+
skipped: false,
|
|
917
|
+
skipReason: null,
|
|
918
|
+
outcome: null,
|
|
919
|
+
error: null,
|
|
920
|
+
phase: null,
|
|
921
|
+
itemType: null,
|
|
922
|
+
itemId: null,
|
|
923
|
+
commandVariant: null,
|
|
924
|
+
sessionId: null,
|
|
925
|
+
lastHeartbeatAt: null,
|
|
926
|
+
baselineModel: null,
|
|
927
|
+
baselineContextTokens: null,
|
|
928
|
+
baselineInputTokens: null,
|
|
929
|
+
baselineCacheCreationTokens: null,
|
|
930
|
+
baselineCacheReadTokens: null,
|
|
931
|
+
turns: null,
|
|
932
|
+
toolCalls: null,
|
|
933
|
+
contextFingerprint: null,
|
|
934
|
+
pluginVersion: null,
|
|
935
|
+
claudeCodeVersion: null,
|
|
936
|
+
phaseId: null,
|
|
937
|
+
inputTokens: null,
|
|
938
|
+
outputTokens: null,
|
|
939
|
+
cacheReadTokens: null,
|
|
940
|
+
cacheCreationTokens: null,
|
|
941
|
+
costUsd: null,
|
|
942
|
+
model: null,
|
|
943
|
+
createdAt: new Date(),
|
|
944
|
+
modelBreakdown: [],
|
|
945
|
+
}),
|
|
946
|
+
},
|
|
947
|
+
agentToolService: {
|
|
948
|
+
list: async () => [],
|
|
949
|
+
add: async () => ({
|
|
950
|
+
id: "t1",
|
|
951
|
+
agentId: COMBINED_AGENT_ID,
|
|
952
|
+
pattern: "Read",
|
|
953
|
+
enabled: true,
|
|
954
|
+
createdAt: new Date(),
|
|
955
|
+
}),
|
|
956
|
+
remove: async () => {},
|
|
957
|
+
toggle: async () => ({
|
|
958
|
+
id: "t1",
|
|
959
|
+
agentId: COMBINED_AGENT_ID,
|
|
960
|
+
pattern: "Read",
|
|
961
|
+
enabled: false,
|
|
962
|
+
createdAt: new Date(),
|
|
963
|
+
}),
|
|
964
|
+
},
|
|
965
|
+
agentTokenService: {
|
|
966
|
+
create: async () => ({
|
|
967
|
+
token: {
|
|
968
|
+
id: "tok1",
|
|
969
|
+
agentId: COMBINED_AGENT_ID,
|
|
970
|
+
token: "hash",
|
|
971
|
+
label: null,
|
|
972
|
+
createdAt: new Date(),
|
|
973
|
+
revokedAt: null,
|
|
974
|
+
},
|
|
975
|
+
rawToken: "raw",
|
|
976
|
+
}),
|
|
977
|
+
listForAgent: async () => [],
|
|
978
|
+
revoke: async () => ({
|
|
979
|
+
id: "tok1",
|
|
980
|
+
agentId: COMBINED_AGENT_ID,
|
|
981
|
+
token: "hash",
|
|
982
|
+
label: null,
|
|
983
|
+
createdAt: new Date(),
|
|
984
|
+
revokedAt: new Date(),
|
|
985
|
+
}),
|
|
986
|
+
validate: async () => null,
|
|
987
|
+
},
|
|
988
|
+
agentPluginService: {
|
|
989
|
+
list: async () => [],
|
|
990
|
+
add: async () => ({
|
|
991
|
+
id: "p1",
|
|
992
|
+
agentId: COMBINED_AGENT_ID,
|
|
993
|
+
name: "plugin",
|
|
994
|
+
version: null,
|
|
995
|
+
enabled: true,
|
|
996
|
+
createdAt: new Date(),
|
|
997
|
+
updatedAt: new Date(),
|
|
998
|
+
}),
|
|
999
|
+
remove: async () => {},
|
|
1000
|
+
removeByName: async () => {},
|
|
1001
|
+
},
|
|
1002
|
+
agentMemberService: {
|
|
1003
|
+
exists: async () => false,
|
|
1004
|
+
listByEmail: async () => [],
|
|
1005
|
+
add: async (agentId: string, email: string) => ({
|
|
1006
|
+
id: "member-1",
|
|
1007
|
+
agentId,
|
|
1008
|
+
email,
|
|
1009
|
+
createdAt: new Date(),
|
|
1010
|
+
}),
|
|
1011
|
+
listByAgentId: async () => [],
|
|
1012
|
+
},
|
|
1013
|
+
agentChatTokenService: {
|
|
1014
|
+
upsertDailyByModel: async (
|
|
1015
|
+
_agentId: string,
|
|
1016
|
+
date: string,
|
|
1017
|
+
model: string,
|
|
1018
|
+
) => ({
|
|
1019
|
+
id: "daily-id",
|
|
1020
|
+
agentId: _agentId,
|
|
1021
|
+
date,
|
|
1022
|
+
model,
|
|
1023
|
+
inputTokens: 0,
|
|
1024
|
+
outputTokens: 0,
|
|
1025
|
+
cacheReadTokens: 0,
|
|
1026
|
+
cacheCreationTokens: 0,
|
|
1027
|
+
costUsd: 0,
|
|
1028
|
+
createdAt: new Date(),
|
|
1029
|
+
updatedAt: new Date(),
|
|
1030
|
+
}),
|
|
1031
|
+
queryStats: async () => ({
|
|
1032
|
+
totals: {
|
|
1033
|
+
input: 0,
|
|
1034
|
+
output: 0,
|
|
1035
|
+
cacheRead: 0,
|
|
1036
|
+
cacheCreation: 0,
|
|
1037
|
+
total: 0,
|
|
1038
|
+
},
|
|
1039
|
+
byAgent: [],
|
|
1040
|
+
byModel: [],
|
|
1041
|
+
daily: [],
|
|
1042
|
+
}),
|
|
1043
|
+
},
|
|
1044
|
+
agentGitHubInstallationsService: {
|
|
1045
|
+
push: async () => {
|
|
1046
|
+
throw new Error("not implemented");
|
|
1047
|
+
},
|
|
1048
|
+
get: async () => null,
|
|
1049
|
+
},
|
|
1050
|
+
agentCronRunStatsService: {
|
|
1051
|
+
outcomes: async () => ({ series: [] }),
|
|
1052
|
+
query: async () => ({
|
|
1053
|
+
totals: {
|
|
1054
|
+
input: 0,
|
|
1055
|
+
output: 0,
|
|
1056
|
+
cacheRead: 0,
|
|
1057
|
+
cacheCreation: 0,
|
|
1058
|
+
total: 0,
|
|
1059
|
+
},
|
|
1060
|
+
byAgent: [],
|
|
1061
|
+
byCron: [],
|
|
1062
|
+
byModel: [],
|
|
1063
|
+
daily: [],
|
|
1064
|
+
byCronModel: [],
|
|
1065
|
+
byPhase: [],
|
|
1066
|
+
}),
|
|
1067
|
+
},
|
|
1068
|
+
prisma: {
|
|
1069
|
+
agent: {
|
|
1070
|
+
create: async () => ({
|
|
1071
|
+
id: "new-id",
|
|
1072
|
+
name: "New",
|
|
1073
|
+
slackId: null,
|
|
1074
|
+
createdAt: new Date(),
|
|
1075
|
+
updatedAt: new Date(),
|
|
1076
|
+
}),
|
|
1077
|
+
},
|
|
1078
|
+
} as never,
|
|
1079
|
+
provisioner: new NoopAgentProvisioner(),
|
|
1080
|
+
serviceIntegrations: [
|
|
1081
|
+
{
|
|
1082
|
+
name: "task-store",
|
|
1083
|
+
listTokensForAgent: async () => [],
|
|
1084
|
+
revokeToken: async () => {},
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
chatService: {
|
|
1088
|
+
listTokensForAgent: async () => [],
|
|
1089
|
+
revokeToken: async () => {},
|
|
1090
|
+
deleteThreadsForAgent: async () => ({ deleted: 0 }),
|
|
1091
|
+
},
|
|
1092
|
+
slack: {
|
|
1093
|
+
deleteApp: async () => {},
|
|
1094
|
+
},
|
|
1095
|
+
decrypt: (value: string) => value,
|
|
1096
|
+
sessionSecret: COMBINED_SESSION_SECRET,
|
|
1097
|
+
adminApiKeys: parseAdminApiKeys(`admin:${COMBINED_ADMIN_KEY}:*`),
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
const root = new Hono();
|
|
1101
|
+
root.get("/health", (c) => c.json({ status: "ok" }));
|
|
1102
|
+
root.get("/", (c) => c.redirect("/admin/login", 302));
|
|
1103
|
+
root.route("/agents", runtimeApp);
|
|
1104
|
+
root.route("/", adminApp);
|
|
1105
|
+
return root;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
describe("root redirect — GET / → 302 /admin/login", () => {
|
|
1109
|
+
test("GET / returns 302 with Location /admin/login", async () => {
|
|
1110
|
+
const root = buildCombinedApp();
|
|
1111
|
+
const res = await root.request("/");
|
|
1112
|
+
expect(res.status).toBe(302);
|
|
1113
|
+
expect(res.headers.get("Location")).toBe("/admin/login");
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
describe("combined server — regression guard: runtime middleware must not block admin CRUD", () => {
|
|
1118
|
+
test("POST /agents/:id/envs with admin key reaches admin handler (201, not 401)", async () => {
|
|
1119
|
+
const root = buildCombinedApp();
|
|
1120
|
+
const res = await root.request(`/agents/${COMBINED_AGENT_ID}/envs`, {
|
|
1121
|
+
method: "POST",
|
|
1122
|
+
body: JSON.stringify({ FOO: "bar" }),
|
|
1123
|
+
headers: {
|
|
1124
|
+
"Content-Type": "application/json",
|
|
1125
|
+
Authorization: `Bearer ${COMBINED_ADMIN_KEY}`,
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
// Both runtimeApp and adminApp use the same admin auth — admin key works for all routes.
|
|
1129
|
+
expect(res.status).toBe(201);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
test("runtime GET /agents/:id/config still requires auth (401 without it)", async () => {
|
|
1133
|
+
const root = buildCombinedApp();
|
|
1134
|
+
const res = await root.request(`/agents/${COMBINED_AGENT_ID}/config`);
|
|
1135
|
+
expect(res.status).toBe(401);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test("runtime GET /agents/:id/config succeeds with admin key (200)", async () => {
|
|
1139
|
+
const root = buildCombinedApp();
|
|
1140
|
+
const res = await root.request(`/agents/${COMBINED_AGENT_ID}/config`, {
|
|
1141
|
+
headers: { Authorization: `Bearer ${COMBINED_ADMIN_KEY}` },
|
|
1142
|
+
});
|
|
1143
|
+
expect(res.status).toBe(200);
|
|
1144
|
+
const body = await res.json();
|
|
1145
|
+
expect(body.repos).toEqual([]);
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
describe("GET /:id/config — Anthropic federation credential strip (WIF-2.1)", () => {
|
|
1150
|
+
const bundleWithCreds: AgentEnvBundle = {
|
|
1151
|
+
agentId: KNOWN_AGENT_ID,
|
|
1152
|
+
env: {
|
|
1153
|
+
SLACK_BOT_TOKEN: "xoxb-secret",
|
|
1154
|
+
CLAUDE_CODE_OAUTH_TOKEN: "sk-ant-oat-secret",
|
|
1155
|
+
ANTHROPIC_API_KEY: "sk-ant-api-secret",
|
|
1156
|
+
ANTHROPIC_AUTH_TOKEN: "auth-secret",
|
|
1157
|
+
ANTHROPIC_MODEL: "claude-test",
|
|
1158
|
+
},
|
|
1159
|
+
allowedTools: [],
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
async function fetchConfig(opts: {
|
|
1163
|
+
selfHosted?: boolean;
|
|
1164
|
+
anthropicFederationEnabled?: boolean;
|
|
1165
|
+
}) {
|
|
1166
|
+
const app = buildApp({ bundleOrNull: bundleWithCreds, ...opts });
|
|
1167
|
+
return app.request(`/${KNOWN_AGENT_ID}/config`, {
|
|
1168
|
+
headers: { Authorization: `Bearer ${VALID_ADMIN_KEY}` },
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
test("federation enabled + non-self-hosted: the three keys are stripped, others kept", async () => {
|
|
1173
|
+
const res = await fetchConfig({
|
|
1174
|
+
selfHosted: false,
|
|
1175
|
+
anthropicFederationEnabled: true,
|
|
1176
|
+
});
|
|
1177
|
+
expect(res.status).toBe(200);
|
|
1178
|
+
const body = await res.json();
|
|
1179
|
+
expect(body.env).toEqual({
|
|
1180
|
+
SLACK_BOT_TOKEN: "xoxb-secret",
|
|
1181
|
+
ANTHROPIC_MODEL: "claude-test",
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test("federation enabled + non-self-hosted: logs agent id and key names, never values", async () => {
|
|
1186
|
+
const logged: string[] = [];
|
|
1187
|
+
const origWarn = console.warn;
|
|
1188
|
+
console.warn = (...args: unknown[]) => {
|
|
1189
|
+
logged.push(args.map(String).join(" "));
|
|
1190
|
+
};
|
|
1191
|
+
try {
|
|
1192
|
+
await fetchConfig({
|
|
1193
|
+
selfHosted: false,
|
|
1194
|
+
anthropicFederationEnabled: true,
|
|
1195
|
+
});
|
|
1196
|
+
} finally {
|
|
1197
|
+
console.warn = origWarn;
|
|
1198
|
+
}
|
|
1199
|
+
const line = logged.join("\n");
|
|
1200
|
+
expect(line).toContain(KNOWN_AGENT_ID);
|
|
1201
|
+
expect(line).toContain("CLAUDE_CODE_OAUTH_TOKEN");
|
|
1202
|
+
expect(line).toContain("ANTHROPIC_API_KEY");
|
|
1203
|
+
expect(line).toContain("ANTHROPIC_AUTH_TOKEN");
|
|
1204
|
+
expect(line).not.toContain("sk-ant-oat-secret");
|
|
1205
|
+
expect(line).not.toContain("sk-ant-api-secret");
|
|
1206
|
+
expect(line).not.toContain("auth-secret");
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test("federation enabled + self-hosted: bundle is untouched", async () => {
|
|
1210
|
+
const res = await fetchConfig({
|
|
1211
|
+
selfHosted: true,
|
|
1212
|
+
anthropicFederationEnabled: true,
|
|
1213
|
+
});
|
|
1214
|
+
const body = await res.json();
|
|
1215
|
+
expect(body.env.ANTHROPIC_API_KEY).toBe("sk-ant-api-secret");
|
|
1216
|
+
expect(body.env.CLAUDE_CODE_OAUTH_TOKEN).toBe("sk-ant-oat-secret");
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
test("federation disabled (default): bundle is untouched", async () => {
|
|
1220
|
+
const res = await fetchConfig({ selfHosted: false });
|
|
1221
|
+
const body = await res.json();
|
|
1222
|
+
expect(body.env.ANTHROPIC_API_KEY).toBe("sk-ant-api-secret");
|
|
1223
|
+
expect(body.env.ANTHROPIC_AUTH_TOKEN).toBe("auth-secret");
|
|
1224
|
+
});
|
|
1225
|
+
});
|