@app-vitals/agent-admin 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +62 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/e2e/chat-thread-page.e2e.ts
|
|
3
|
+
* Admin UI — Chat Thread Page E2E Tests (CFB-1.3)
|
|
4
|
+
*
|
|
5
|
+
* Proves the inline-style → class migration produced no visual change: at a
|
|
6
|
+
* desktop viewport, `.chat-bubble-inner`'s computed width must still be 70%
|
|
7
|
+
* of its containing block (`#messages-container`), matching the value that
|
|
8
|
+
* used to be set via an inline `style="max-width:70%"` before the migration
|
|
9
|
+
* moved it into the `.chat-bubble-inner` CSS class.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - Spawns admin/e2e/test-server.ts via Bun as a child process.
|
|
13
|
+
* - Uses hono/jwt sign() to mint valid session cookies directly.
|
|
14
|
+
* - No real DB or OAuth flow is initiated.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { type BrowserContext, type Page, expect, test } from "@playwright/test";
|
|
21
|
+
import { sign } from "hono/jwt";
|
|
22
|
+
|
|
23
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const PORT = 3492;
|
|
26
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
27
|
+
const SESSION_SECRET = "e2e-admin-test-secret-32chars!!!";
|
|
28
|
+
const SESSION_COOKIE = "admin_session";
|
|
29
|
+
const AGENT_ID = "agent-e2e-1";
|
|
30
|
+
// Matches MOCK_CHAT_THREAD.id in test-server.ts.
|
|
31
|
+
const THREAD_ID = "thread-e2e-1";
|
|
32
|
+
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
let serverProcess: ChildProcess | null = null;
|
|
38
|
+
|
|
39
|
+
async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
while (Date.now() - start < maxWaitMs) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url);
|
|
44
|
+
if (res.status < 500) return;
|
|
45
|
+
} catch {
|
|
46
|
+
// Not ready yet
|
|
47
|
+
}
|
|
48
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Server at ${url} did not start within ${maxWaitMs}ms`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test.beforeAll(async () => {
|
|
54
|
+
const serverScript = resolve(__dirname, "test-server.ts");
|
|
55
|
+
|
|
56
|
+
serverProcess = spawn("bun", ["run", serverScript], {
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
ADMIN_E2E_PORT: String(PORT),
|
|
60
|
+
ADMIN_E2E_SESSION_SECRET: SESSION_SECRET,
|
|
61
|
+
},
|
|
62
|
+
stdio: "pipe",
|
|
63
|
+
cwd: resolve(__dirname, "../.."),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
serverProcess.stderr?.on("data", (data: Buffer) => {
|
|
67
|
+
const msg = data.toString();
|
|
68
|
+
if (msg.trim()) console.error("[admin-e2e-chat-thread-server]", msg.trim());
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
serverProcess.stdout?.on("data", (data: Buffer) => {
|
|
72
|
+
const msg = data.toString();
|
|
73
|
+
if (msg.trim()) console.log("[admin-e2e-chat-thread-server]", msg.trim());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitForServer(`${BASE_URL}/health`, 15_000);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test.afterAll(() => {
|
|
80
|
+
if (serverProcess) {
|
|
81
|
+
serverProcess.kill("SIGTERM");
|
|
82
|
+
serverProcess = null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── Session cookie helper ────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function mintSession(
|
|
89
|
+
userId = "google-sub-e2e",
|
|
90
|
+
email = "admin@example.com",
|
|
91
|
+
): Promise<string> {
|
|
92
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
93
|
+
return sign(
|
|
94
|
+
{ userId, email, isAdmin: true, iat: nowSec, exp: nowSec + 3600 },
|
|
95
|
+
SESSION_SECRET,
|
|
96
|
+
"HS256",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Helper: load chat thread page with auth ──────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function loadChatThreadPage(
|
|
103
|
+
page: Page,
|
|
104
|
+
context: BrowserContext,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const token = await mintSession();
|
|
107
|
+
await context.addCookies([
|
|
108
|
+
{
|
|
109
|
+
name: SESSION_COOKIE,
|
|
110
|
+
value: token,
|
|
111
|
+
domain: "localhost",
|
|
112
|
+
path: "/",
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
sameSite: "Lax",
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
await page.goto(`${BASE_URL}/admin/chat/${AGENT_ID}/threads/${THREAD_ID}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
test.describe("GET /admin/chat/:agentId/threads/:threadId — CFB-1.3 class migration", () => {
|
|
123
|
+
test("page renders the fixture message inside a .chat-bubble-inner element", async ({
|
|
124
|
+
page,
|
|
125
|
+
context,
|
|
126
|
+
}) => {
|
|
127
|
+
await loadChatThreadPage(page, context);
|
|
128
|
+
await expect(page.locator(".chat-bubble-inner").first()).toBeVisible();
|
|
129
|
+
await expect(page.locator("body")).toContainText(
|
|
130
|
+
"Hello from the e2e test suite",
|
|
131
|
+
{ useInnerText: true },
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("no style attribute on the migrated elements (page wrapper, header, messages container, bubble, bubble inner)", async ({
|
|
136
|
+
page,
|
|
137
|
+
context,
|
|
138
|
+
}) => {
|
|
139
|
+
await loadChatThreadPage(page, context);
|
|
140
|
+
|
|
141
|
+
await expect(
|
|
142
|
+
page.locator(".vos-page.chat-thread-page"),
|
|
143
|
+
).not.toHaveAttribute("style", /.+/);
|
|
144
|
+
await expect(
|
|
145
|
+
page.locator(".page-header.chat-thread-header"),
|
|
146
|
+
).not.toHaveAttribute("style", /.+/);
|
|
147
|
+
await expect(page.locator("#messages-container")).not.toHaveAttribute(
|
|
148
|
+
"style",
|
|
149
|
+
/.+/,
|
|
150
|
+
);
|
|
151
|
+
await expect(page.locator(".chat-bubble").first()).not.toHaveAttribute(
|
|
152
|
+
"style",
|
|
153
|
+
/.+/,
|
|
154
|
+
);
|
|
155
|
+
await expect(
|
|
156
|
+
page.locator(".chat-bubble-inner").first(),
|
|
157
|
+
).not.toHaveAttribute("style", /.+/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("at 1280px viewport, .chat-bubble-inner computed width is 70% of #messages-container", async ({
|
|
161
|
+
page,
|
|
162
|
+
context,
|
|
163
|
+
}) => {
|
|
164
|
+
await page.setViewportSize({ width: 1280, height: 900 });
|
|
165
|
+
await loadChatThreadPage(page, context);
|
|
166
|
+
|
|
167
|
+
const bubbleInner = page.locator(".chat-bubble-inner").first();
|
|
168
|
+
await expect(bubbleInner).toBeVisible();
|
|
169
|
+
|
|
170
|
+
const bubbleWidth = await bubbleInner.evaluate(
|
|
171
|
+
(el) => el.getBoundingClientRect().width,
|
|
172
|
+
);
|
|
173
|
+
const containerWidth = await page
|
|
174
|
+
.locator("#messages-container")
|
|
175
|
+
.evaluate((el) => el.getBoundingClientRect().width);
|
|
176
|
+
|
|
177
|
+
const ratio = bubbleWidth / containerWidth;
|
|
178
|
+
// The bubble inner's max-width:70% caps it at 70% of the container, but
|
|
179
|
+
// an unconstrained (narrow) message could render narrower still — assert
|
|
180
|
+
// it's at/near the 70% cap, not merely "less than" some looser bound.
|
|
181
|
+
expect(ratio).toBeGreaterThan(0.65);
|
|
182
|
+
expect(ratio).toBeLessThanOrEqual(0.71);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("at 375px mobile viewport, .chat-bubble-inner computed width is 90% of #messages-container", async ({
|
|
186
|
+
page,
|
|
187
|
+
context,
|
|
188
|
+
}) => {
|
|
189
|
+
// Regression guard: chatPageStyles's @media (max-width:640px) override
|
|
190
|
+
// (.chat-bubble-inner { max-width:90% }) must win the cascade over
|
|
191
|
+
// chatThreadStyles's unconditional base rule (max-width:70%) at mobile
|
|
192
|
+
// widths — see CFB-1.3 review discussion on concatenation order.
|
|
193
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
194
|
+
await loadChatThreadPage(page, context);
|
|
195
|
+
|
|
196
|
+
const bubbleInner = page.locator(".chat-bubble-inner").first();
|
|
197
|
+
await expect(bubbleInner).toBeVisible();
|
|
198
|
+
|
|
199
|
+
const bubbleWidth = await bubbleInner.evaluate(
|
|
200
|
+
(el) => el.getBoundingClientRect().width,
|
|
201
|
+
);
|
|
202
|
+
const containerWidth = await page
|
|
203
|
+
.locator("#messages-container")
|
|
204
|
+
.evaluate((el) => el.getBoundingClientRect().width);
|
|
205
|
+
|
|
206
|
+
const ratio = bubbleWidth / containerWidth;
|
|
207
|
+
expect(ratio).toBeGreaterThan(0.85);
|
|
208
|
+
expect(ratio).toBeLessThanOrEqual(0.91);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── CFB-3.1 — mobile responsive defects ───────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
test.describe("GET /admin/chat/:agentId/threads/:threadId — CFB-3.1 mobile responsive", () => {
|
|
215
|
+
test("at 375px, every visible input/textarea/select computes font-size >= 16px", async ({
|
|
216
|
+
page,
|
|
217
|
+
context,
|
|
218
|
+
}) => {
|
|
219
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
220
|
+
await loadChatThreadPage(page, context);
|
|
221
|
+
|
|
222
|
+
const fontSizes = await page.evaluate(() => {
|
|
223
|
+
const els = Array.from(
|
|
224
|
+
document.querySelectorAll("input, textarea, select"),
|
|
225
|
+
) as HTMLElement[];
|
|
226
|
+
return els
|
|
227
|
+
.filter((el) => {
|
|
228
|
+
const style = getComputedStyle(el);
|
|
229
|
+
return (
|
|
230
|
+
style.display !== "none" &&
|
|
231
|
+
style.visibility !== "hidden" &&
|
|
232
|
+
(el as HTMLInputElement).type !== "hidden" &&
|
|
233
|
+
(el as HTMLInputElement).type !== "checkbox" &&
|
|
234
|
+
(el as HTMLInputElement).type !== "file"
|
|
235
|
+
);
|
|
236
|
+
})
|
|
237
|
+
.map((el) => ({
|
|
238
|
+
id: el.id || el.tagName,
|
|
239
|
+
fontSize: Number.parseFloat(getComputedStyle(el).fontSize),
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(fontSizes.length).toBeGreaterThan(0);
|
|
244
|
+
for (const { id, fontSize } of fontSizes) {
|
|
245
|
+
expect(fontSize, `${id} font-size`).toBeGreaterThanOrEqual(16);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("at 375px and 768px, no horizontal overflow", async ({
|
|
250
|
+
page,
|
|
251
|
+
context,
|
|
252
|
+
}) => {
|
|
253
|
+
for (const width of [375, 768]) {
|
|
254
|
+
await page.setViewportSize({ width, height: 812 });
|
|
255
|
+
await loadChatThreadPage(page, context);
|
|
256
|
+
|
|
257
|
+
const { scrollWidth, clientWidth } = await page.evaluate(() => ({
|
|
258
|
+
scrollWidth: document.documentElement.scrollWidth,
|
|
259
|
+
clientWidth: document.documentElement.clientWidth,
|
|
260
|
+
}));
|
|
261
|
+
expect(scrollWidth, `width=${width}`).toBeLessThanOrEqual(clientWidth);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("at a mobile viewport, touch targets are >= 44px except .data-table .btn", async ({
|
|
266
|
+
page,
|
|
267
|
+
context,
|
|
268
|
+
}) => {
|
|
269
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
270
|
+
await loadChatThreadPage(page, context);
|
|
271
|
+
|
|
272
|
+
const heights = await page.evaluate(() => {
|
|
273
|
+
const els = Array.from(
|
|
274
|
+
document.querySelectorAll(".btn, .thread-pane-link"),
|
|
275
|
+
) as HTMLElement[];
|
|
276
|
+
return els
|
|
277
|
+
.filter((el) => {
|
|
278
|
+
const rect = el.getBoundingClientRect();
|
|
279
|
+
// offsetParent is null when the element (or an ancestor) is
|
|
280
|
+
// display:none — e.g. .chat-thread-sidebar collapses on mobile,
|
|
281
|
+
// which zeroes out its descendants' rects without those elements
|
|
282
|
+
// themselves being display:none. Skip anything not actually
|
|
283
|
+
// rendered rather than asserting on a phantom 0-height box.
|
|
284
|
+
return el.offsetParent !== null && rect.width > 0 && rect.height > 0;
|
|
285
|
+
})
|
|
286
|
+
.map((el) => ({
|
|
287
|
+
className: el.className,
|
|
288
|
+
height: el.getBoundingClientRect().height,
|
|
289
|
+
}));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(heights.length).toBeGreaterThan(0);
|
|
293
|
+
for (const { className, height } of heights) {
|
|
294
|
+
expect(height, className).toBeGreaterThanOrEqual(44);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("the rename/delete forms are collapsed behind a <details> element by default", async ({
|
|
299
|
+
page,
|
|
300
|
+
context,
|
|
301
|
+
}) => {
|
|
302
|
+
await loadChatThreadPage(page, context);
|
|
303
|
+
|
|
304
|
+
const details = page.locator("details.chat-thread-actions");
|
|
305
|
+
await expect(details).toBeAttached();
|
|
306
|
+
const isOpen = await details.evaluate(
|
|
307
|
+
(el) => (el as HTMLDetailsElement).open,
|
|
308
|
+
);
|
|
309
|
+
expect(isOpen).toBe(false);
|
|
310
|
+
|
|
311
|
+
// The rename/delete forms exist in the DOM but aren't visible until expanded.
|
|
312
|
+
await expect(page.locator('form[action*="/rename"]')).not.toBeVisible();
|
|
313
|
+
await expect(page.locator('form[action*="/delete"]')).not.toBeVisible();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("the chat-thread-page layout does not collapse to zero height at a mobile viewport", async ({
|
|
317
|
+
page,
|
|
318
|
+
context,
|
|
319
|
+
}) => {
|
|
320
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
321
|
+
await loadChatThreadPage(page, context);
|
|
322
|
+
|
|
323
|
+
const pageHeight = await page
|
|
324
|
+
.locator(".chat-thread-page")
|
|
325
|
+
.evaluate((el) => el.getBoundingClientRect().height);
|
|
326
|
+
expect(pageHeight).toBeGreaterThan(400);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ─── CFB-3.2 — mobile drawer, sticky composer, collapsed header ────────────────
|
|
331
|
+
|
|
332
|
+
test.describe("GET /admin/chat/:agentId/threads/:threadId — CFB-3.2 mobile drawer", () => {
|
|
333
|
+
// Criterion #1: the drawer opens and closes at 375px, with a scrim.
|
|
334
|
+
test("at 375px the drawer opens via the hamburger and a scrim appears, then closes", async ({
|
|
335
|
+
page,
|
|
336
|
+
context,
|
|
337
|
+
}) => {
|
|
338
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
339
|
+
await loadChatThreadPage(page, context);
|
|
340
|
+
|
|
341
|
+
const sidebar = page.locator(".chat-thread-sidebar");
|
|
342
|
+
const scrim = page.locator(".chat-drawer-scrim");
|
|
343
|
+
|
|
344
|
+
// Closed by default: the sidebar is off-canvas (its left edge is at or
|
|
345
|
+
// beyond the left viewport edge, i.e. translated out of view) and the
|
|
346
|
+
// scrim is not interactable.
|
|
347
|
+
const closedLeft = await sidebar.evaluate(
|
|
348
|
+
(el) => el.getBoundingClientRect().left,
|
|
349
|
+
);
|
|
350
|
+
expect(closedLeft).toBeLessThan(0);
|
|
351
|
+
await expect(scrim).not.toBeVisible();
|
|
352
|
+
|
|
353
|
+
// Open it via the hamburger label.
|
|
354
|
+
await page.locator(".chat-drawer-hamburger").click();
|
|
355
|
+
|
|
356
|
+
await expect(scrim).toBeVisible();
|
|
357
|
+
// The drawer slides in via a 0.2s transform transition — poll until its
|
|
358
|
+
// left edge settles on-screen rather than sampling mid-animation.
|
|
359
|
+
await expect
|
|
360
|
+
.poll(async () =>
|
|
361
|
+
sidebar.evaluate((el) => el.getBoundingClientRect().left),
|
|
362
|
+
)
|
|
363
|
+
.toBeGreaterThanOrEqual(0);
|
|
364
|
+
|
|
365
|
+
// Clicking the scrim closes it again. The drawer (z-index 60) overlaps the
|
|
366
|
+
// left ~280px of the full-viewport scrim, so click the scrim on its
|
|
367
|
+
// uncovered right edge rather than its (covered) center.
|
|
368
|
+
await scrim.click({ position: { x: 340, y: 400 } });
|
|
369
|
+
await expect(scrim).not.toBeVisible();
|
|
370
|
+
await expect
|
|
371
|
+
.poll(async () =>
|
|
372
|
+
sidebar.evaluate((el) => el.getBoundingClientRect().left),
|
|
373
|
+
)
|
|
374
|
+
.toBeLessThan(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Criterion #5: aria-expanded flips and Escape closes the drawer.
|
|
378
|
+
test("aria-expanded flips on open and Escape closes the drawer", async ({
|
|
379
|
+
page,
|
|
380
|
+
context,
|
|
381
|
+
}) => {
|
|
382
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
383
|
+
await loadChatThreadPage(page, context);
|
|
384
|
+
|
|
385
|
+
const hamburger = page.locator(".chat-drawer-hamburger");
|
|
386
|
+
const toggle = page.locator("#chat-drawer-toggle");
|
|
387
|
+
|
|
388
|
+
expect(await hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
389
|
+
|
|
390
|
+
await hamburger.click();
|
|
391
|
+
expect(await toggle.isChecked()).toBe(true);
|
|
392
|
+
expect(await hamburger.getAttribute("aria-expanded")).toBe("true");
|
|
393
|
+
|
|
394
|
+
await page.keyboard.press("Escape");
|
|
395
|
+
expect(await toggle.isChecked()).toBe(false);
|
|
396
|
+
expect(await hamburger.getAttribute("aria-expanded")).toBe("false");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Criterion #4: header bounding height <= 96px and messages container top
|
|
400
|
+
// < 200px at 375px.
|
|
401
|
+
test("at 375px the header is <= 96px tall and messages start above 200px", async ({
|
|
402
|
+
page,
|
|
403
|
+
context,
|
|
404
|
+
}) => {
|
|
405
|
+
await page.setViewportSize({ width: 375, height: 812 });
|
|
406
|
+
await loadChatThreadPage(page, context);
|
|
407
|
+
|
|
408
|
+
const headerHeight = await page
|
|
409
|
+
.locator(".chat-thread-header")
|
|
410
|
+
.evaluate((el) => el.getBoundingClientRect().height);
|
|
411
|
+
expect(headerHeight).toBeLessThanOrEqual(96);
|
|
412
|
+
|
|
413
|
+
const messagesTop = await page
|
|
414
|
+
.locator("#messages-container")
|
|
415
|
+
.evaluate((el) => el.getBoundingClientRect().top);
|
|
416
|
+
expect(messagesTop).toBeLessThan(200);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Criterion #3: composer stays in the viewport with the keyboard open
|
|
420
|
+
// (proxied by shrinking the viewport height to 400).
|
|
421
|
+
test("composer stays within the viewport when height shrinks to 400", async ({
|
|
422
|
+
page,
|
|
423
|
+
context,
|
|
424
|
+
}) => {
|
|
425
|
+
await page.setViewportSize({ width: 375, height: 400 });
|
|
426
|
+
await loadChatThreadPage(page, context);
|
|
427
|
+
|
|
428
|
+
const { composerBottom, viewportHeight } = await page.evaluate(() => {
|
|
429
|
+
const form = document.querySelector(
|
|
430
|
+
".chat-composer-form",
|
|
431
|
+
) as HTMLElement | null;
|
|
432
|
+
return {
|
|
433
|
+
composerBottom: form
|
|
434
|
+
? form.getBoundingClientRect().bottom
|
|
435
|
+
: Number.POSITIVE_INFINITY,
|
|
436
|
+
viewportHeight: window.innerHeight,
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// The composer's bottom edge must not fall below the visible viewport.
|
|
441
|
+
// A 1px rounding tolerance guards against sub-pixel layout.
|
|
442
|
+
expect(composerBottom).toBeLessThanOrEqual(viewportHeight + 1);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/e2e/login-page.e2e.ts
|
|
3
|
+
* Admin UI — Login Page E2E Tests (ADM-3.2)
|
|
4
|
+
*
|
|
5
|
+
* Tests the static render of GET /admin/login, covering:
|
|
6
|
+
* 1. "Sign in with Google" button is present
|
|
7
|
+
* 2. Error banner visible for ?error=invalid_state
|
|
8
|
+
* 3. Error banner visible for ?error=access_denied
|
|
9
|
+
* 4. Error banner visible for ?error=auth_failed
|
|
10
|
+
* 5. Error banner visible for ?error=server_error
|
|
11
|
+
* 6. No error banner when no ?error= param
|
|
12
|
+
* 7. ?returnTo= param is preserved in the OAuth button href
|
|
13
|
+
* 8. No ?returnTo= → OAuth button href is plain /admin/auth/google
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* - Spawns admin/e2e/test-server.ts via Bun as a child process.
|
|
17
|
+
* - The /admin/login route is a pure static render — no DB access.
|
|
18
|
+
* - No real OAuth flow is initiated.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
22
|
+
import { dirname, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { expect, test } from "@playwright/test";
|
|
25
|
+
|
|
26
|
+
// ─── Port ─────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const PORT = 3490;
|
|
29
|
+
const BASE_URL = `http://localhost:${PORT}`;
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
|
|
33
|
+
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
let serverProcess: ChildProcess | null = null;
|
|
36
|
+
|
|
37
|
+
async function waitForServer(url: string, maxWaitMs: number): Promise<void> {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
while (Date.now() - start < maxWaitMs) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url);
|
|
42
|
+
if (res.status < 500) return;
|
|
43
|
+
} catch {
|
|
44
|
+
// Not ready yet
|
|
45
|
+
}
|
|
46
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Server at ${url} failed to start within ${maxWaitMs}ms`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test.beforeAll(async () => {
|
|
52
|
+
const serverScript = resolve(__dirname, "test-server.ts");
|
|
53
|
+
|
|
54
|
+
serverProcess = spawn("bun", ["run", serverScript], {
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
ADMIN_E2E_PORT: String(PORT),
|
|
58
|
+
},
|
|
59
|
+
stdio: "pipe",
|
|
60
|
+
cwd: resolve(__dirname, "../.."),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
serverProcess.stderr?.on("data", (data: Buffer) => {
|
|
64
|
+
const msg = data.toString();
|
|
65
|
+
if (msg.trim()) console.error("[admin-e2e-login-server]", msg.trim());
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await waitForServer(`${BASE_URL}/health`, 15_000);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test.afterAll(() => {
|
|
72
|
+
if (serverProcess) {
|
|
73
|
+
serverProcess.kill("SIGTERM");
|
|
74
|
+
serverProcess = null;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
test.describe("GET /admin/login — static render", () => {
|
|
81
|
+
test('renders "Sign in with Google" button', async ({ page }) => {
|
|
82
|
+
await page.goto(`${BASE_URL}/admin/login`);
|
|
83
|
+
|
|
84
|
+
const btn = page.locator(".btn.btn-primary");
|
|
85
|
+
await expect(btn).toBeVisible();
|
|
86
|
+
await expect(btn).toContainText("Sign in with Google");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("no error banner when no ?error= param", async ({ page }) => {
|
|
90
|
+
await page.goto(`${BASE_URL}/admin/login`);
|
|
91
|
+
await expect(page.locator(".alert.alert-error")).not.toBeVisible();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("?error=invalid_state shows error banner", async ({ page }) => {
|
|
95
|
+
await page.goto(`${BASE_URL}/admin/login?error=invalid_state`);
|
|
96
|
+
const banner = page.locator(".alert.alert-error");
|
|
97
|
+
await expect(banner).toBeVisible();
|
|
98
|
+
await expect(banner).toContainText("invalid_state");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("?error=access_denied shows error banner", async ({ page }) => {
|
|
102
|
+
await page.goto(`${BASE_URL}/admin/login?error=access_denied`);
|
|
103
|
+
const banner = page.locator(".alert.alert-error");
|
|
104
|
+
await expect(banner).toBeVisible();
|
|
105
|
+
await expect(banner).toContainText("access_denied");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("?error=auth_failed shows error banner", async ({ page }) => {
|
|
109
|
+
await page.goto(`${BASE_URL}/admin/login?error=auth_failed`);
|
|
110
|
+
const banner = page.locator(".alert.alert-error");
|
|
111
|
+
await expect(banner).toBeVisible();
|
|
112
|
+
await expect(banner).toContainText("auth_failed");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("?error=server_error shows error banner", async ({ page }) => {
|
|
116
|
+
await page.goto(`${BASE_URL}/admin/login?error=server_error`);
|
|
117
|
+
const banner = page.locator(".alert.alert-error");
|
|
118
|
+
await expect(banner).toBeVisible();
|
|
119
|
+
await expect(banner).toContainText("server_error");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("?returnTo= param is preserved in OAuth button href", async ({
|
|
123
|
+
page,
|
|
124
|
+
}) => {
|
|
125
|
+
const returnTo = "/admin/agents";
|
|
126
|
+
await page.goto(
|
|
127
|
+
`${BASE_URL}/admin/login?returnTo=${encodeURIComponent(returnTo)}`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const btn = page.locator(".btn.btn-primary");
|
|
131
|
+
const href = await btn.getAttribute("href");
|
|
132
|
+
|
|
133
|
+
expect(href).not.toBeNull();
|
|
134
|
+
expect(href).toContain("/admin/auth/google");
|
|
135
|
+
expect(href).toContain(`returnTo=${encodeURIComponent(returnTo)}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("no ?returnTo= → OAuth button href is plain /admin/auth/google", async ({
|
|
139
|
+
page,
|
|
140
|
+
}) => {
|
|
141
|
+
await page.goto(`${BASE_URL}/admin/login`);
|
|
142
|
+
|
|
143
|
+
const btn = page.locator(".btn.btn-primary");
|
|
144
|
+
const href = await btn.getAttribute("href");
|
|
145
|
+
|
|
146
|
+
expect(href).toBe("/admin/auth/google");
|
|
147
|
+
});
|
|
148
|
+
});
|