@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
package/Dockerfile
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Admin service image (ghcr.io/app-vitals/agent-admin)
|
|
2
|
+
#
|
|
3
|
+
# Build from the repository root: docker build -f admin/Dockerfile .
|
|
4
|
+
|
|
5
|
+
FROM oven/bun:1-slim AS deps
|
|
6
|
+
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
COPY package.json bunfig.toml bun.lock* ./
|
|
10
|
+
COPY admin/package.json ./admin/
|
|
11
|
+
COPY admin/prisma ./admin/prisma/
|
|
12
|
+
COPY admin/prisma.config.ts ./admin/
|
|
13
|
+
COPY runtime/package.json ./runtime/
|
|
14
|
+
COPY lib/package.json ./lib/
|
|
15
|
+
COPY patches/ ./patches/
|
|
16
|
+
|
|
17
|
+
RUN if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi
|
|
18
|
+
|
|
19
|
+
FROM oven/bun:1-slim AS production
|
|
20
|
+
|
|
21
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
22
|
+
ca-certificates \
|
|
23
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24
|
+
|
|
25
|
+
WORKDIR /app
|
|
26
|
+
|
|
27
|
+
ENV NODE_ENV=production
|
|
28
|
+
|
|
29
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
30
|
+
COPY --from=deps /app/admin/node_modules ./admin/node_modules
|
|
31
|
+
COPY --from=deps /app/lib/node_modules ./lib/node_modules
|
|
32
|
+
|
|
33
|
+
# .dockerignore keeps node_modules, .git and build output out of the context.
|
|
34
|
+
COPY . .
|
|
35
|
+
|
|
36
|
+
# The generated Prisma client is not committed.
|
|
37
|
+
RUN cd admin && bunx prisma generate --schema=prisma/schema
|
|
38
|
+
|
|
39
|
+
EXPOSE 3000
|
|
40
|
+
|
|
41
|
+
USER 1000
|
|
42
|
+
|
|
43
|
+
ENTRYPOINT ["bun", "run", "admin/src/main.ts"]
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Shipwright Admin Service
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The admin service provides a web UI and CRUD API for managing Shipwright agents — env vars, cron jobs, tokens, tools, and plugins. It also exposes the runtime API used by deployed agents to fetch their configuration.
|
|
4
|
+
|
|
5
|
+
The HTTP server is implemented in `admin/src/main.ts`, which composes the admin UI, admin CRUD API, and agent runtime API into a single Hono app running on port 3000.
|
|
6
|
+
|
|
7
|
+
## Running Locally
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From repo root — install all workspace deps
|
|
11
|
+
bun install
|
|
12
|
+
|
|
13
|
+
# Export required env vars (see Environment Variables table below)
|
|
14
|
+
export DATABASE_URL_SHIPWRIGHT_ADMIN=postgresql://...
|
|
15
|
+
export SHIPWRIGHT_SESSION_SECRET=...
|
|
16
|
+
# ... (remaining vars)
|
|
17
|
+
|
|
18
|
+
# Run the admin server
|
|
19
|
+
bun run admin/src/main.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The service listens on `http://localhost:3000` by default. Visit `/admin` for the UI.
|
|
23
|
+
|
|
24
|
+
## Environment Variables
|
|
25
|
+
|
|
26
|
+
| Variable | Required | Description |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `DATABASE_URL_SHIPWRIGHT_ADMIN` | Yes | PostgreSQL connection string for the admin Prisma schema |
|
|
29
|
+
| `SHIPWRIGHT_SESSION_SECRET` | Yes | JWT cookie secret for admin UI sessions |
|
|
30
|
+
| `GOOGLE_CLIENT_ID` | Yes | Google OAuth 2.0 client ID for admin UI login |
|
|
31
|
+
| `GOOGLE_CLIENT_SECRET` | Yes | Google OAuth 2.0 client secret |
|
|
32
|
+
| `SHIPWRIGHT_ADMIN_ALLOWED_EMAILS` | Yes | Comma-separated list of emails permitted to log in |
|
|
33
|
+
| `SHIPWRIGHT_ADMIN_APP_BASE_URL` | Yes | Public base URL used for OAuth redirect (defaults to `http://localhost:3000`) |
|
|
34
|
+
| `SHIPWRIGHT_ADMIN_API_KEYS` | No | Comma-separated `name:token:scope` tuples for bearer token auth. Scope is `*`, an agent id, `platform.read` or `platform.ops` (no colons in scopes) — see [docs/agent-api.md](../docs/agent-api.md) |
|
|
35
|
+
| `SHIPWRIGHT_ENCRYPTION_KEY` | Recommended | 64-char hex key for AES-256-GCM encryption of stored secrets |
|
|
36
|
+
| `PORT` | No | HTTP server port (defaults to `3000`) |
|
|
37
|
+
|
|
38
|
+
## Database
|
|
39
|
+
|
|
40
|
+
The admin service uses its own PostgreSQL database. Run migrations before starting:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export DATABASE_URL_SHIPWRIGHT_ADMIN=postgres://...
|
|
44
|
+
cd admin && bunx prisma migrate deploy --schema=prisma/schema
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Building the Docker Image
|
|
48
|
+
|
|
49
|
+
Build from the repo root (the Dockerfile uses the full monorepo context):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
docker build -f admin/Dockerfile -t shipwright-admin .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run the image:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
docker run \
|
|
59
|
+
-e DATABASE_URL_SHIPWRIGHT_ADMIN=postgres://... \
|
|
60
|
+
-e SHIPWRIGHT_SESSION_SECRET=... \
|
|
61
|
+
-e GOOGLE_CLIENT_ID=... \
|
|
62
|
+
-e GOOGLE_CLIENT_SECRET=... \
|
|
63
|
+
-e SHIPWRIGHT_ADMIN_ALLOWED_EMAILS=admin@example.com \
|
|
64
|
+
-e SHIPWRIGHT_ADMIN_APP_BASE_URL=https://admin.example.com \
|
|
65
|
+
-p 3000:3000 \
|
|
66
|
+
shipwright-admin
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Deployment
|
|
70
|
+
|
|
71
|
+
A GKE Deployment + Service manifest is provided at [`admin/deploy/deployment.yaml`](./deploy/deployment.yaml).
|
|
72
|
+
|
|
73
|
+
It expects a Kubernetes Secret named `shipwright-admin-secret` containing all required env vars. See the comments at the top of the manifest for setup instructions.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/e2e/agents-page.e2e.ts
|
|
3
|
+
* Admin UI — Agents Page E2E Tests (ADM-3.2)
|
|
4
|
+
*
|
|
5
|
+
* Tests the agent detail page at GET /admin/agents/:id, covering:
|
|
6
|
+
* 1. Unauthenticated GET /admin/agents → redirects to /admin/login
|
|
7
|
+
* 2. Authenticated → page title contains agent name
|
|
8
|
+
* 3. Authenticated → h1 contains agent name
|
|
9
|
+
* 4. Authenticated → agent card shows agent name "Test Agent"
|
|
10
|
+
* 5. Env var key "SLACK_BOT_TOKEN" is visible
|
|
11
|
+
* 6. Env var value is masked (••••••••), raw secret not present in page
|
|
12
|
+
* 7. Cron job schedule "0 9 * * 1" is visible
|
|
13
|
+
* 8. Cron job prompt is visible
|
|
14
|
+
* 9. Token label "Production token" is visible
|
|
15
|
+
* 10. Env var add form is present (action contains /envs)
|
|
16
|
+
* 11. Add cron form is present (action contains /crons)
|
|
17
|
+
* 12. Token form is present (action contains /tokens)
|
|
18
|
+
* 13. Mobile viewport (375px) — no horizontal overflow
|
|
19
|
+
*
|
|
20
|
+
* Architecture:
|
|
21
|
+
* - Spawns admin/e2e/test-server.ts via Bun as a child process.
|
|
22
|
+
* - Uses hono/jwt sign() to mint valid session cookies directly.
|
|
23
|
+
* - No real DB or OAuth flow is initiated.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
27
|
+
import { dirname, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { type BrowserContext, type Page, expect, test } from "@playwright/test";
|
|
30
|
+
import { sign } from "hono/jwt";
|
|
31
|
+
|
|
32
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const PORT = 3491;
|
|
35
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
36
|
+
const SESSION_SECRET = "e2e-admin-test-secret-32chars!!!";
|
|
37
|
+
const SESSION_COOKIE = "admin_session";
|
|
38
|
+
const AGENT_ID = "agent-e2e-1";
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
|
|
42
|
+
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
let serverProcess: ChildProcess | null = null;
|
|
45
|
+
|
|
46
|
+
async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
while (Date.now() - start < maxWaitMs) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url);
|
|
51
|
+
if (res.status < 500) return;
|
|
52
|
+
} catch {
|
|
53
|
+
// Not ready yet
|
|
54
|
+
}
|
|
55
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Server at ${url} did not start within ${maxWaitMs}ms`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test.beforeAll(async () => {
|
|
61
|
+
const serverScript = resolve(__dirname, "test-server.ts");
|
|
62
|
+
|
|
63
|
+
serverProcess = spawn("bun", ["run", serverScript], {
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
ADMIN_E2E_PORT: String(PORT),
|
|
67
|
+
ADMIN_E2E_SESSION_SECRET: SESSION_SECRET,
|
|
68
|
+
},
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
cwd: resolve(__dirname, "../.."),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
serverProcess.stderr?.on("data", (data: Buffer) => {
|
|
74
|
+
const msg = data.toString();
|
|
75
|
+
if (msg.trim()) console.error("[admin-e2e-agents-server]", msg.trim());
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
serverProcess.stdout?.on("data", (data: Buffer) => {
|
|
79
|
+
const msg = data.toString();
|
|
80
|
+
if (msg.trim()) console.log("[admin-e2e-agents-server]", msg.trim());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await waitForServer(`${BASE_URL}/health`, 15_000);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.afterAll(() => {
|
|
87
|
+
if (serverProcess) {
|
|
88
|
+
serverProcess.kill("SIGTERM");
|
|
89
|
+
serverProcess = null;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Session cookie helper ────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function mintSession(
|
|
96
|
+
userId = "google-sub-e2e",
|
|
97
|
+
email = "admin@example.com",
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
100
|
+
return sign(
|
|
101
|
+
{ userId, email, isAdmin: true, iat: nowSec, exp: nowSec + 3600 },
|
|
102
|
+
SESSION_SECRET,
|
|
103
|
+
"HS256",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Helper: load agent detail page with auth ─────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async function loadAgentDetailPage(
|
|
110
|
+
page: Page,
|
|
111
|
+
context: BrowserContext,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const token = await mintSession();
|
|
114
|
+
await context.addCookies([
|
|
115
|
+
{
|
|
116
|
+
name: SESSION_COOKIE,
|
|
117
|
+
value: token,
|
|
118
|
+
domain: "localhost",
|
|
119
|
+
path: "/",
|
|
120
|
+
httpOnly: true,
|
|
121
|
+
sameSite: "Lax",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
await page.goto(`${BASE_URL}/admin/agents/${AGENT_ID}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── 1. Unauthenticated redirect ──────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
test.describe("GET /admin/agents — unauthenticated", () => {
|
|
130
|
+
test("redirects to /admin/login when no session cookie", async ({ page }) => {
|
|
131
|
+
await page.goto(`${BASE_URL}/admin/agents`);
|
|
132
|
+
await expect(page).toHaveURL(/\/admin\/login/);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── 2–13. Authenticated tests ────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
test.describe("GET /admin/agents/:id — authenticated", () => {
|
|
139
|
+
test("page title contains agent name", async ({ page, context }) => {
|
|
140
|
+
await loadAgentDetailPage(page, context);
|
|
141
|
+
await expect(page).toHaveTitle(/Test Agent/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("h1 contains agent name", async ({ page, context }) => {
|
|
145
|
+
await loadAgentDetailPage(page, context);
|
|
146
|
+
await expect(page.locator("h1")).toContainText("Test Agent");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('agent name "Test Agent" is visible on the page', async ({
|
|
150
|
+
page,
|
|
151
|
+
context,
|
|
152
|
+
}) => {
|
|
153
|
+
await loadAgentDetailPage(page, context);
|
|
154
|
+
await expect(page.locator("body")).toContainText("Test Agent");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('env var key "SLACK_BOT_TOKEN" is visible', async ({
|
|
158
|
+
page,
|
|
159
|
+
context,
|
|
160
|
+
}) => {
|
|
161
|
+
await loadAgentDetailPage(page, context);
|
|
162
|
+
await expect(page.locator("body")).toContainText("SLACK_BOT_TOKEN");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("env var value is masked — shows •••••••• and not the raw secret", async ({
|
|
166
|
+
page,
|
|
167
|
+
context,
|
|
168
|
+
}) => {
|
|
169
|
+
await loadAgentDetailPage(page, context);
|
|
170
|
+
const body = await page.locator("body").textContent();
|
|
171
|
+
expect(body).toContain("••••••••");
|
|
172
|
+
expect(body).not.toContain("xoxb-secret-value");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('cron job schedule "0 9 * * 1" is visible', async ({
|
|
176
|
+
page,
|
|
177
|
+
context,
|
|
178
|
+
}) => {
|
|
179
|
+
await loadAgentDetailPage(page, context);
|
|
180
|
+
await expect(page.locator("body")).toContainText("0 9 * * 1");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("cron job prompt is visible", async ({ page, context }) => {
|
|
184
|
+
await loadAgentDetailPage(page, context);
|
|
185
|
+
await expect(page.locator("body")).toContainText("Send the weekly standup");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('token label "Production token" is visible', async ({
|
|
189
|
+
page,
|
|
190
|
+
context,
|
|
191
|
+
}) => {
|
|
192
|
+
await loadAgentDetailPage(page, context);
|
|
193
|
+
await expect(page.locator("body")).toContainText("Production token");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("env var add form is present (action contains /envs)", async ({
|
|
197
|
+
page,
|
|
198
|
+
context,
|
|
199
|
+
}) => {
|
|
200
|
+
await loadAgentDetailPage(page, context);
|
|
201
|
+
const envForm = page.locator('form[action*="/envs"]').first();
|
|
202
|
+
await expect(envForm).toBeAttached();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("add cron form is present (action contains /crons)", async ({
|
|
206
|
+
page,
|
|
207
|
+
context,
|
|
208
|
+
}) => {
|
|
209
|
+
await loadAgentDetailPage(page, context);
|
|
210
|
+
const cronForm = page.locator('form[action*="/crons"]').first();
|
|
211
|
+
await expect(cronForm).toBeAttached();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("token form is present (action contains /tokens)", async ({
|
|
215
|
+
page,
|
|
216
|
+
context,
|
|
217
|
+
}) => {
|
|
218
|
+
await loadAgentDetailPage(page, context);
|
|
219
|
+
const tokenForm = page.locator('form[action*="/tokens"]').first();
|
|
220
|
+
await expect(tokenForm).toBeAttached();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("mobile viewport (375px) — page renders without horizontal overflow", async ({
|
|
224
|
+
page,
|
|
225
|
+
context,
|
|
226
|
+
}) => {
|
|
227
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
228
|
+
await loadAgentDetailPage(page, context);
|
|
229
|
+
|
|
230
|
+
const docScrollWidth = await page.evaluate(
|
|
231
|
+
() => document.documentElement.scrollWidth,
|
|
232
|
+
);
|
|
233
|
+
const innerWidth = await page.evaluate(() => window.innerWidth);
|
|
234
|
+
expect(docScrollWidth).toBeLessThanOrEqual(innerWidth + 1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("specialty name is visible on the detail page", async ({
|
|
238
|
+
page,
|
|
239
|
+
context,
|
|
240
|
+
}) => {
|
|
241
|
+
await loadAgentDetailPage(page, context);
|
|
242
|
+
await expect(page.locator("body")).toContainText("coding");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// CFB-3.1: .data-table .btn must stay at its current ~36px height at mobile
|
|
246
|
+
// widths — the mobile-wide .btn min-height:44px rule must not cascade
|
|
247
|
+
// through to buttons inside a .data-table (e.g. the env-var "Delete"
|
|
248
|
+
// button), or the tasks/PRs/env tables would become absurdly tall.
|
|
249
|
+
test("at 375px, a .data-table .btn (env var Delete button) stays near its current ~36px height, not 44px", async ({
|
|
250
|
+
page,
|
|
251
|
+
context,
|
|
252
|
+
}) => {
|
|
253
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
254
|
+
await loadAgentDetailPage(page, context);
|
|
255
|
+
|
|
256
|
+
// Env Vars now lives inside a collapsed accordion group (AGA-1.1) —
|
|
257
|
+
// expand it before asserting on the visibility of anything inside.
|
|
258
|
+
await page
|
|
259
|
+
.locator("#env-vars")
|
|
260
|
+
.locator("xpath=ancestor::details[contains(@class, 'group')]")
|
|
261
|
+
.first()
|
|
262
|
+
.evaluate((details) => {
|
|
263
|
+
(details as HTMLDetailsElement).open = true;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const deleteBtn = page
|
|
267
|
+
.locator("#env-vars .data-table .btn", { hasText: "Delete" })
|
|
268
|
+
.first();
|
|
269
|
+
await expect(deleteBtn).toBeVisible();
|
|
270
|
+
|
|
271
|
+
const height = await deleteBtn.evaluate(
|
|
272
|
+
(el) => el.getBoundingClientRect().height,
|
|
273
|
+
);
|
|
274
|
+
expect(height).toBeLessThan(44);
|
|
275
|
+
expect(height).toBeGreaterThan(0);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ─── New-specialty picker (ATS-4.1) ──────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
async function loadNewAgentPage(
|
|
282
|
+
page: Page,
|
|
283
|
+
context: BrowserContext,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const token = await mintSession();
|
|
286
|
+
await context.addCookies([
|
|
287
|
+
{
|
|
288
|
+
name: SESSION_COOKIE,
|
|
289
|
+
value: token,
|
|
290
|
+
domain: "localhost",
|
|
291
|
+
path: "/",
|
|
292
|
+
httpOnly: true,
|
|
293
|
+
sameSite: "Lax",
|
|
294
|
+
},
|
|
295
|
+
]);
|
|
296
|
+
await page.goto(`${BASE_URL}/admin/agents/new`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
test.describe("GET /admin/agents/new — type picker", () => {
|
|
300
|
+
test("a required select[name=type] is present", async ({ page, context }) => {
|
|
301
|
+
await loadNewAgentPage(page, context);
|
|
302
|
+
const typeSelect = page.locator('select[name="type"]');
|
|
303
|
+
await expect(typeSelect).toBeAttached();
|
|
304
|
+
await expect(typeSelect).toHaveAttribute("required", "");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('the select lists "Coding Agent" (value coding)', async ({
|
|
308
|
+
page,
|
|
309
|
+
context,
|
|
310
|
+
}) => {
|
|
311
|
+
await loadNewAgentPage(page, context);
|
|
312
|
+
const codingOption = page.locator(
|
|
313
|
+
'select[name="type"] option[value="coding"]',
|
|
314
|
+
);
|
|
315
|
+
await expect(codingOption).toBeAttached();
|
|
316
|
+
await expect(codingOption).toHaveText("Coding Agent");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test.describe("POST /admin/agents — create-with-type journey", () => {
|
|
321
|
+
test("filling the form (incl. type) and submitting lands on the new agent's detail page showing its type", async ({
|
|
322
|
+
page,
|
|
323
|
+
context,
|
|
324
|
+
}) => {
|
|
325
|
+
await loadNewAgentPage(page, context);
|
|
326
|
+
|
|
327
|
+
await page.fill('input[name="name"]', "My New Coding Agent");
|
|
328
|
+
await page.selectOption('select[name="type"]', "coding");
|
|
329
|
+
// Scoped to the new-agent form specifically — the page also has a
|
|
330
|
+
// "Sign out" button[type="submit"] in the toolbar, and an unscoped
|
|
331
|
+
// selector matches that one first (it's earlier in the DOM), which
|
|
332
|
+
// logs the session out instead of submitting this form.
|
|
333
|
+
await page.click('form[action="/admin/agents"] button[type="submit"]');
|
|
334
|
+
|
|
335
|
+
await page.waitForURL(/\/admin\/agents\/[^/]+$/);
|
|
336
|
+
await expect(page.locator("body")).toContainText("coding");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/e2e/chat-degraded.e2e.ts
|
|
3
|
+
* Admin UI — Chat Degraded Mode E2E Tests (CFB-2.3)
|
|
4
|
+
*
|
|
5
|
+
* Proves that `chatClient` is a genuinely injectable dependency of the test
|
|
6
|
+
* server, not a hardcoded mock — and that the three other chat e2e suites
|
|
7
|
+
* (chat-page.e2e.ts, chat-thread-page.e2e.ts, chat-progress.e2e.ts) are
|
|
8
|
+
* exercising the real chat UI rather than silently passing against a
|
|
9
|
+
* degraded fallback page.
|
|
10
|
+
*
|
|
11
|
+
* When the server is spawned with ADMIN_E2E_CHAT_CLIENT_ABSENT=1, test-server.ts
|
|
12
|
+
* builds AdminUIDeps with chatClient: undefined, which is the same condition
|
|
13
|
+
* production hits when SHIPWRIGHT_CHAT_SERVICE_URL /
|
|
14
|
+
* SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN are unset. Both /admin/chat and the
|
|
15
|
+
* per-thread route must then render the "Chat service not configured." alert
|
|
16
|
+
* instead of the real thread list / message UI.
|
|
17
|
+
*
|
|
18
|
+
* Architecture mirrors chat-progress.e2e.ts: spawns admin/e2e/test-server.ts
|
|
19
|
+
* via Bun as a child process, mints session cookies via hono/jwt.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
23
|
+
import { dirname, resolve } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { type BrowserContext, expect, test } from "@playwright/test";
|
|
26
|
+
import { sign } from "hono/jwt";
|
|
27
|
+
|
|
28
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const PORT = 3495;
|
|
31
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
32
|
+
const SESSION_SECRET = "e2e-admin-test-secret-32chars!!!";
|
|
33
|
+
const SESSION_COOKIE = "admin_session";
|
|
34
|
+
const AGENT_ID = "agent-e2e-1";
|
|
35
|
+
// Matches MOCK_CHAT_THREAD.id in test-server.ts.
|
|
36
|
+
const THREAD_ID = "thread-e2e-1";
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
|
|
40
|
+
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
let serverProcess: ChildProcess | null = null;
|
|
43
|
+
|
|
44
|
+
async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
while (Date.now() - start < maxWaitMs) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(url);
|
|
49
|
+
if (res.status < 500) return;
|
|
50
|
+
} catch {
|
|
51
|
+
// Not ready yet
|
|
52
|
+
}
|
|
53
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Server at ${url} did not start within ${maxWaitMs}ms`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test.beforeAll(async () => {
|
|
59
|
+
const serverScript = resolve(__dirname, "test-server.ts");
|
|
60
|
+
|
|
61
|
+
serverProcess = spawn("bun", ["run", serverScript], {
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
ADMIN_E2E_PORT: String(PORT),
|
|
65
|
+
ADMIN_E2E_SESSION_SECRET: SESSION_SECRET,
|
|
66
|
+
// Configure this spawned server to run with no chatClient at all, so
|
|
67
|
+
// the /admin/chat* routes fall back to degraded-mode rendering.
|
|
68
|
+
ADMIN_E2E_CHAT_CLIENT_ABSENT: "1",
|
|
69
|
+
},
|
|
70
|
+
stdio: "pipe",
|
|
71
|
+
cwd: resolve(__dirname, "../.."),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
serverProcess.stderr?.on("data", (data: Buffer) => {
|
|
75
|
+
const msg = data.toString();
|
|
76
|
+
if (msg.trim()) console.error("[admin-e2e-chat-degraded-server]", msg.trim());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
serverProcess.stdout?.on("data", (data: Buffer) => {
|
|
80
|
+
const msg = data.toString();
|
|
81
|
+
if (msg.trim()) console.log("[admin-e2e-chat-degraded-server]", msg.trim());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await waitForServer(`${BASE_URL}/health`, 15_000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test.afterAll(() => {
|
|
88
|
+
if (serverProcess) {
|
|
89
|
+
serverProcess.kill("SIGTERM");
|
|
90
|
+
serverProcess = null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── Session cookie helper ────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
async function mintSession(
|
|
97
|
+
userId = "google-sub-e2e",
|
|
98
|
+
email = "admin@example.com",
|
|
99
|
+
): Promise<string> {
|
|
100
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
101
|
+
return sign(
|
|
102
|
+
{ userId, email, isAdmin: true, iat: nowSec, exp: nowSec + 3600 },
|
|
103
|
+
SESSION_SECRET,
|
|
104
|
+
"HS256",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function authenticate(context: BrowserContext): Promise<void> {
|
|
109
|
+
const token = await mintSession();
|
|
110
|
+
await context.addCookies([
|
|
111
|
+
{
|
|
112
|
+
name: SESSION_COOKIE,
|
|
113
|
+
value: token,
|
|
114
|
+
domain: "localhost",
|
|
115
|
+
path: "/",
|
|
116
|
+
httpOnly: true,
|
|
117
|
+
sameSite: "Lax",
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test.describe("GET /admin/chat* — CFB-2.3 degraded mode (chatClient absent)", () => {
|
|
125
|
+
test("the chat list page renders the degraded alert instead of the thread list", async ({
|
|
126
|
+
page,
|
|
127
|
+
context,
|
|
128
|
+
}) => {
|
|
129
|
+
await authenticate(context);
|
|
130
|
+
await page.goto(`${BASE_URL}/admin/chat?agentId=${AGENT_ID}`);
|
|
131
|
+
|
|
132
|
+
await expect(page.locator(".alert.alert-error")).toContainText(
|
|
133
|
+
"Chat service not configured.",
|
|
134
|
+
);
|
|
135
|
+
await expect(page.locator(".thread-pane-list")).toHaveCount(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("the thread detail page renders the degraded alert instead of the message UI", async ({
|
|
139
|
+
page,
|
|
140
|
+
context,
|
|
141
|
+
}) => {
|
|
142
|
+
await authenticate(context);
|
|
143
|
+
await page.goto(
|
|
144
|
+
`${BASE_URL}/admin/chat/${AGENT_ID}/threads/${THREAD_ID}`,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
await expect(page.locator(".alert.alert-error")).toContainText(
|
|
148
|
+
"Chat service not configured.",
|
|
149
|
+
);
|
|
150
|
+
await expect(page.locator("#live-status-bubble")).toHaveCount(0);
|
|
151
|
+
await expect(page.locator("#messages-container")).toHaveCount(0);
|
|
152
|
+
});
|
|
153
|
+
});
|