@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,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/pwa.unit.test.ts
|
|
3
|
+
* Unit tests for the PWA shell's pure functions: manifest building, service
|
|
4
|
+
* worker body generation, the precache allowlist, and the shouldCache
|
|
5
|
+
* predicate. No I/O — everything here takes plain inputs and returns data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
buildManifest,
|
|
11
|
+
buildOfflinePageHtml,
|
|
12
|
+
buildServiceWorkerBody,
|
|
13
|
+
getPrecacheList,
|
|
14
|
+
PWA_ICONS,
|
|
15
|
+
renderPwaHeadTags,
|
|
16
|
+
sanitizeStartUrl,
|
|
17
|
+
shouldCachePwaRequest,
|
|
18
|
+
} from "./pwa.ts";
|
|
19
|
+
import tokens from "./theme-tokens.json";
|
|
20
|
+
|
|
21
|
+
describe("getPrecacheList", () => {
|
|
22
|
+
it("is exactly the offline page plus the six icons — nothing else", () => {
|
|
23
|
+
const list = getPrecacheList();
|
|
24
|
+
const expected = [
|
|
25
|
+
"/admin/offline.html",
|
|
26
|
+
...PWA_ICONS.map((icon) => `/admin/icons/${icon.filename}`),
|
|
27
|
+
];
|
|
28
|
+
expect(list.sort()).toEqual(expected.sort());
|
|
29
|
+
expect(list.length).toBe(7);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("contains no HTML document routes other than the offline fallback", () => {
|
|
33
|
+
const list = getPrecacheList();
|
|
34
|
+
const nonOfflineHtml = list.filter(
|
|
35
|
+
(url) => url.endsWith(".html") && url !== "/admin/offline.html",
|
|
36
|
+
);
|
|
37
|
+
expect(nonOfflineHtml).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("contains no .json routes", () => {
|
|
41
|
+
const list = getPrecacheList();
|
|
42
|
+
expect(list.some((url) => url.endsWith(".json"))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("contains nothing under /admin/chat/", () => {
|
|
46
|
+
const list = getPrecacheList();
|
|
47
|
+
expect(list.some((url) => url.startsWith("/admin/chat/"))).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("shouldCachePwaRequest", () => {
|
|
52
|
+
it("allows GET requests for precached icon paths", () => {
|
|
53
|
+
expect(shouldCachePwaRequest("/admin/icons/icon-192.png", "GET")).toBe(
|
|
54
|
+
true,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("allows GET for the offline fallback page", () => {
|
|
59
|
+
expect(shouldCachePwaRequest("/admin/offline.html", "GET")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("never caches document navigations (other HTML paths)", () => {
|
|
63
|
+
expect(shouldCachePwaRequest("/admin/agents", "GET")).toBe(false);
|
|
64
|
+
expect(shouldCachePwaRequest("/admin/agents/abc123", "GET")).toBe(false);
|
|
65
|
+
expect(shouldCachePwaRequest("/admin/login", "GET")).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("never caches any .json route", () => {
|
|
69
|
+
expect(shouldCachePwaRequest("/admin/manifest.webmanifest", "GET")).toBe(
|
|
70
|
+
false,
|
|
71
|
+
);
|
|
72
|
+
expect(shouldCachePwaRequest("/agents/config.json", "GET")).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("never caches anything under /admin/chat/", () => {
|
|
76
|
+
expect(shouldCachePwaRequest("/admin/chat/", "GET")).toBe(false);
|
|
77
|
+
expect(shouldCachePwaRequest("/admin/chat/thread-1", "GET")).toBe(false);
|
|
78
|
+
expect(shouldCachePwaRequest("/admin/chat/thread-1/messages", "GET")).toBe(
|
|
79
|
+
false,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("never caches non-GET requests, even for allowlisted paths", () => {
|
|
84
|
+
expect(shouldCachePwaRequest("/admin/icons/icon-192.png", "POST")).toBe(
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
expect(shouldCachePwaRequest("/admin/offline.html", "PUT")).toBe(false);
|
|
88
|
+
expect(shouldCachePwaRequest("/admin/offline.html", "DELETE")).toBe(false);
|
|
89
|
+
expect(shouldCachePwaRequest("/admin/offline.html", "HEAD")).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("never caches an arbitrary unknown path", () => {
|
|
93
|
+
expect(shouldCachePwaRequest("/admin/some-random-thing", "GET")).toBe(
|
|
94
|
+
false,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("PWA_ICONS", () => {
|
|
100
|
+
it("declares exactly six icons", () => {
|
|
101
|
+
expect(PWA_ICONS.length).toBe(6);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("includes at least one maskable icon and the apple-touch-icon", () => {
|
|
105
|
+
expect(PWA_ICONS.some((i) => i.purpose === "maskable")).toBe(true);
|
|
106
|
+
expect(PWA_ICONS.some((i) => i.filename === "apple-touch-icon.png")).toBe(
|
|
107
|
+
true,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("buildManifest", () => {
|
|
113
|
+
it("sets scope /admin/, start_url /admin/chat, and display standalone", () => {
|
|
114
|
+
const manifest = buildManifest();
|
|
115
|
+
expect(manifest.scope).toBe("/admin/");
|
|
116
|
+
expect(manifest.start_url).toBe("/admin/chat");
|
|
117
|
+
expect(manifest.display).toBe("standalone");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("sources theme_color and background_color from brand/tokens.json", () => {
|
|
121
|
+
const manifest = buildManifest();
|
|
122
|
+
expect(manifest.theme_color).toBe(tokens.color.brand.default);
|
|
123
|
+
expect(manifest.background_color).toBe(tokens.color.bg.base);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("declares icons entries with any + maskable purposes and correct src paths", () => {
|
|
127
|
+
const manifest = buildManifest();
|
|
128
|
+
expect(manifest.icons.length).toBe(
|
|
129
|
+
PWA_ICONS.filter((i) => i.purpose !== "apple-touch-icon").length,
|
|
130
|
+
);
|
|
131
|
+
for (const icon of manifest.icons) {
|
|
132
|
+
expect(icon.src.startsWith("/admin/icons/")).toBe(true);
|
|
133
|
+
expect(["any", "maskable"]).toContain(icon.purpose);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("uses a caller-supplied startUrl as start_url (PWA-1.1)", () => {
|
|
138
|
+
const manifest = buildManifest("/admin/tasks");
|
|
139
|
+
expect(manifest.start_url).toBe("/admin/tasks");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("still defaults to /admin/chat when startUrl is omitted (backward compatible)", () => {
|
|
143
|
+
const manifest = buildManifest(undefined);
|
|
144
|
+
expect(manifest.start_url).toBe("/admin/chat");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("sanitizeStartUrl (PWA-1.1)", () => {
|
|
149
|
+
it("falls back to /admin/chat for null, undefined, and empty input", () => {
|
|
150
|
+
expect(sanitizeStartUrl(null)).toBe("/admin/chat");
|
|
151
|
+
expect(sanitizeStartUrl(undefined)).toBe("/admin/chat");
|
|
152
|
+
expect(sanitizeStartUrl("")).toBe("/admin/chat");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("passes through same-origin /admin/... paths unchanged", () => {
|
|
156
|
+
expect(sanitizeStartUrl("/admin/chat")).toBe("/admin/chat");
|
|
157
|
+
expect(sanitizeStartUrl("/admin/tasks")).toBe("/admin/tasks");
|
|
158
|
+
expect(sanitizeStartUrl("/admin/agents/123")).toBe("/admin/agents/123");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects bare /admin with no trailing in-scope content", () => {
|
|
162
|
+
expect(sanitizeStartUrl("/admin")).toBe("/admin/chat");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("rejects external absolute URLs", () => {
|
|
166
|
+
expect(sanitizeStartUrl("https://evil.com")).toBe("/admin/chat");
|
|
167
|
+
expect(sanitizeStartUrl("http://evil.com/admin/chat")).toBe("/admin/chat");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("rejects protocol-relative URLs", () => {
|
|
171
|
+
expect(sanitizeStartUrl("//evil.com/x")).toBe("/admin/chat");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("rejects javascript: URLs", () => {
|
|
175
|
+
expect(sanitizeStartUrl("javascript:alert(1)")).toBe("/admin/chat");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("rejects path traversal, raw and encoded", () => {
|
|
179
|
+
expect(sanitizeStartUrl("/admin/../etc/passwd")).toBe("/admin/chat");
|
|
180
|
+
expect(sanitizeStartUrl("/admin/tasks/../../etc/passwd")).toBe(
|
|
181
|
+
"/admin/chat",
|
|
182
|
+
);
|
|
183
|
+
expect(sanitizeStartUrl("/admin/%2e%2e/etc/passwd")).toBe("/admin/chat");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("rejects malformed percent-encoding instead of throwing", () => {
|
|
187
|
+
expect(sanitizeStartUrl("/admin/%zz")).toBe("/admin/chat");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("rejects a path not under /admin/", () => {
|
|
191
|
+
expect(sanitizeStartUrl("/tasks")).toBe("/admin/chat");
|
|
192
|
+
expect(sanitizeStartUrl("/")).toBe("/admin/chat");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("rejects the PWA shell's own non-navigable asset routes", () => {
|
|
196
|
+
expect(sanitizeStartUrl("/admin/manifest.webmanifest")).toBe("/admin/chat");
|
|
197
|
+
expect(sanitizeStartUrl("/admin/sw.js")).toBe("/admin/chat");
|
|
198
|
+
expect(sanitizeStartUrl("/admin/offline.html")).toBe("/admin/chat");
|
|
199
|
+
expect(sanitizeStartUrl("/admin/icons/icon-192.png")).toBe("/admin/chat");
|
|
200
|
+
expect(sanitizeStartUrl("/admin/icons/apple-touch-icon.png")).toBe(
|
|
201
|
+
"/admin/chat",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("rejects the PWA shell's own auth routes", () => {
|
|
206
|
+
expect(sanitizeStartUrl("/admin/login")).toBe("/admin/chat");
|
|
207
|
+
expect(sanitizeStartUrl("/admin/logout")).toBe("/admin/chat");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("rejects OAuth callback routes for every provider", () => {
|
|
211
|
+
expect(sanitizeStartUrl("/admin/auth/callback")).toBe("/admin/chat");
|
|
212
|
+
expect(sanitizeStartUrl("/admin/auth/okta/callback")).toBe("/admin/chat");
|
|
213
|
+
expect(
|
|
214
|
+
sanitizeStartUrl("/admin/agents/abc123/connect-slack/callback"),
|
|
215
|
+
).toBe("/admin/chat");
|
|
216
|
+
expect(
|
|
217
|
+
sanitizeStartUrl("/admin/agents/abc123/connect-github/callback"),
|
|
218
|
+
).toBe("/admin/chat");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("buildServiceWorkerBody", () => {
|
|
223
|
+
it("interpolates the version into the cache name", () => {
|
|
224
|
+
const body = buildServiceWorkerBody("1.200.0", getPrecacheList());
|
|
225
|
+
expect(body).toContain("shipwright-admin-pwa-v1.200.0");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("embeds exactly the precache list, not an arbitrary superset", () => {
|
|
229
|
+
const precache = getPrecacheList();
|
|
230
|
+
const body = buildServiceWorkerBody("1.200.0", precache);
|
|
231
|
+
for (const url of precache) {
|
|
232
|
+
expect(body).toContain(JSON.stringify(url));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("is a pure function — repeated calls with the same input are identical", () => {
|
|
237
|
+
const precache = getPrecacheList();
|
|
238
|
+
const a = buildServiceWorkerBody("1.200.0", precache);
|
|
239
|
+
const b = buildServiceWorkerBody("1.200.0", precache);
|
|
240
|
+
expect(a).toBe(b);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("different versions bust the cache name", () => {
|
|
244
|
+
const precache = getPrecacheList();
|
|
245
|
+
const a = buildServiceWorkerBody("1.200.0", precache);
|
|
246
|
+
const b = buildServiceWorkerBody("1.201.0", precache);
|
|
247
|
+
expect(a).not.toBe(b);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("buildServiceWorkerBody — push + notificationclick (CFB-4.2)", () => {
|
|
252
|
+
it("registers a push listener that shows a notification", () => {
|
|
253
|
+
const body = buildServiceWorkerBody("1.200.0", getPrecacheList());
|
|
254
|
+
expect(body).toContain('addEventListener("push"');
|
|
255
|
+
expect(body).toContain("showNotification");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("registers a notificationclick listener that focuses/opens the thread", () => {
|
|
259
|
+
const body = buildServiceWorkerBody("1.200.0", getPrecacheList());
|
|
260
|
+
expect(body).toContain('addEventListener("notificationclick"');
|
|
261
|
+
expect(body).toContain("clients.openWindow");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("reads the deep-link url from the notification payload data", () => {
|
|
265
|
+
const body = buildServiceWorkerBody("1.200.0", getPrecacheList());
|
|
266
|
+
// The push payload is JSON {title, body, url}; the SW must parse it.
|
|
267
|
+
expect(body).toContain("event.data");
|
|
268
|
+
expect(body).toContain(".url");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("reads payload.tag for the notification tag, falling back to the shipwright-agent-reply default (SESH-7.3)", () => {
|
|
272
|
+
const body = buildServiceWorkerBody("1.200.0", getPrecacheList());
|
|
273
|
+
// A session-scoped payload (tag: "shipwright-session-x") must produce a
|
|
274
|
+
// distinctly-tagged notification so two session payloads never coalesce;
|
|
275
|
+
// a tagless chat payload must still fall back to the existing default.
|
|
276
|
+
expect(body).toContain("payload.tag");
|
|
277
|
+
expect(body).toContain('tag: payload.tag || "shipwright-agent-reply"');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("buildOfflinePageHtml", () => {
|
|
282
|
+
it("renders a self-contained document with no external requests", () => {
|
|
283
|
+
const html = buildOfflinePageHtml();
|
|
284
|
+
expect(html).toContain("<!DOCTYPE html>");
|
|
285
|
+
expect(html).not.toContain("<script");
|
|
286
|
+
expect(html).not.toContain("<link");
|
|
287
|
+
expect(html).not.toContain("<img");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("is a pure function — deterministic output", () => {
|
|
291
|
+
expect(buildOfflinePageHtml()).toBe(buildOfflinePageHtml());
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("renderPwaHeadTags", () => {
|
|
296
|
+
it("includes the manifest link and SW registration script for an https appBaseUrl", () => {
|
|
297
|
+
const head = renderPwaHeadTags("https://admin.example.com");
|
|
298
|
+
expect(head).toContain('rel="manifest"');
|
|
299
|
+
expect(head).toContain("/admin/manifest.webmanifest");
|
|
300
|
+
expect(head).toContain("serviceWorker");
|
|
301
|
+
expect(head).toContain("/admin/sw.js");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("is suppressed entirely for a plain-http appBaseUrl (home-lab operators)", () => {
|
|
305
|
+
const head = renderPwaHeadTags("http://localhost:3001");
|
|
306
|
+
expect(head).toBe("");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("is suppressed for a malformed/empty appBaseUrl", () => {
|
|
310
|
+
expect(renderPwaHeadTags("")).toBe("");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("gives the manifest link a stable id and rewrites its href to the current pathname on load (PWA-1.1)", () => {
|
|
314
|
+
const head = renderPwaHeadTags("https://admin.example.com");
|
|
315
|
+
expect(head).toContain('id="pwa-manifest-link"');
|
|
316
|
+
expect(head).toContain("getElementById('pwa-manifest-link')");
|
|
317
|
+
expect(head).toContain("location.pathname");
|
|
318
|
+
expect(head).toContain("?start=");
|
|
319
|
+
expect(head).toContain("encodeURIComponent");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Reads the schema file and asserts it uses the postgresql provider and the namespaced env var.
|
|
6
|
+
// This test is intentionally narrow — it guards against accidental provider regression.
|
|
7
|
+
|
|
8
|
+
// The admin schema is a Prisma multi-file schema folder (prisma/schema/):
|
|
9
|
+
// schema.prisma holds the generator/datasource + shared models, coding.prisma
|
|
10
|
+
// the coding-owned models. Assertions run against the concatenation.
|
|
11
|
+
const schemaDir = join(import.meta.dir, "../prisma/schema");
|
|
12
|
+
const schema = readdirSync(schemaDir)
|
|
13
|
+
.filter((f) => f.endsWith(".prisma"))
|
|
14
|
+
.sort()
|
|
15
|
+
.map((f) => readFileSync(join(schemaDir, f), "utf-8"))
|
|
16
|
+
.join("\n");
|
|
17
|
+
|
|
18
|
+
const configPath = join(import.meta.dir, "../prisma.config.ts");
|
|
19
|
+
const config = readFileSync(configPath, "utf-8");
|
|
20
|
+
|
|
21
|
+
describe("admin/prisma/schema/", () => {
|
|
22
|
+
test('datasource provider is "postgresql"', () => {
|
|
23
|
+
expect(schema).toContain('provider = "postgresql"');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('generator provider is "prisma-client"', () => {
|
|
27
|
+
// Prisma 7 replaced the `prisma-client-js` generator with `prisma-client`;
|
|
28
|
+
// regressing to the old one would also silently re-break every
|
|
29
|
+
// `../prisma/client/client.ts` import.
|
|
30
|
+
expect(schema).toContain('provider = "prisma-client"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("datasource block declares no url (Prisma 7 moved it to prisma.config.ts)", () => {
|
|
34
|
+
expect(schema).toMatch(/datasource db \{\s*provider = "postgresql"\s*\}/);
|
|
35
|
+
expect(schema).not.toContain("url = env(");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("prisma.config.ts sources the url from DATABASE_URL_SHIPWRIGHT_ADMIN", () => {
|
|
39
|
+
expect(config).toContain("process.env.DATABASE_URL_SHIPWRIGHT_ADMIN");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("AgentCronRun model", () => {
|
|
43
|
+
test("model AgentCronRun exists in schema", () => {
|
|
44
|
+
expect(schema).toContain("model AgentCronRun");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("id field is String @id @default(cuid())", () => {
|
|
48
|
+
expect(schema).toMatch(
|
|
49
|
+
/model AgentCronRun \{[\s\S]*?id\s+String\s+@id\s+@default\(cuid\(\)\)/,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("cronId field with @relation to AgentCronJob and onDelete: Cascade", () => {
|
|
54
|
+
expect(schema).toMatch(/model AgentCronRun \{[\s\S]*?cronId\s+String/);
|
|
55
|
+
expect(schema).toMatch(
|
|
56
|
+
/model AgentCronRun \{[\s\S]*?@relation\(fields: \[cronId\], references: \[id\], onDelete: Cascade\)/,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("agentId field is String (denormalized)", () => {
|
|
61
|
+
expect(schema).toMatch(/model AgentCronRun \{[\s\S]*?agentId\s+String/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("startedAt field is DateTime", () => {
|
|
65
|
+
expect(schema).toMatch(
|
|
66
|
+
/model AgentCronRun \{[\s\S]*?startedAt\s+DateTime/,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("completedAt field is DateTime? (nullable)", () => {
|
|
71
|
+
expect(schema).toMatch(
|
|
72
|
+
/model AgentCronRun \{[\s\S]*?completedAt\s+DateTime\?/,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("skipped field is Boolean @default(false)", () => {
|
|
77
|
+
expect(schema).toMatch(
|
|
78
|
+
/model AgentCronRun \{[\s\S]*?skipped\s+Boolean\s+@default\(false\)/,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("skipReason field is String? (nullable)", () => {
|
|
83
|
+
expect(schema).toMatch(
|
|
84
|
+
/model AgentCronRun \{[\s\S]*?skipReason\s+String\?/,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("outcome field is String? (nullable)", () => {
|
|
89
|
+
expect(schema).toMatch(/model AgentCronRun \{[\s\S]*?outcome\s+String\?/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("error field is String? (nullable)", () => {
|
|
93
|
+
expect(schema).toMatch(/model AgentCronRun \{[\s\S]*?error\s+String\?/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("createdAt field is DateTime @default(now())", () => {
|
|
97
|
+
expect(schema).toMatch(
|
|
98
|
+
/model AgentCronRun \{[\s\S]*?createdAt\s+DateTime\s+@default\(now\(\)\)/,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("@@index([cronId]) present", () => {
|
|
103
|
+
expect(schema).toMatch(
|
|
104
|
+
/model AgentCronRun \{[\s\S]*?@@index\(\[cronId\]\)/,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("@@index([agentId, startedAt]) present", () => {
|
|
109
|
+
expect(schema).toMatch(
|
|
110
|
+
/model AgentCronRun \{[\s\S]*?@@index\(\[agentId, startedAt\]\)/,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("AgentCronJob back-relation", () => {
|
|
116
|
+
test("AgentCronJob has runs AgentCronRun[] back-relation", () => {
|
|
117
|
+
expect(schema).toMatch(
|
|
118
|
+
/model AgentCronJob \{[\s\S]*?runs\s+AgentCronRun\[\]/,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/self-serve-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure parser for the self-serve agent provisioning env config. Takes an
|
|
5
|
+
* injected env object (no process.env reads) so it is deterministically
|
|
6
|
+
* unit-testable; admin/src/main.ts reads it once at startup and injects the
|
|
7
|
+
* resolved config as deps.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_SELF_SERVE_CONTACT_EMAIL = "dan@app-vitals.com";
|
|
11
|
+
|
|
12
|
+
/** Structural env shape — accepts `process.env` or a plain test object. */
|
|
13
|
+
type SelfServeEnv = Readonly<Record<string, string | undefined>>;
|
|
14
|
+
|
|
15
|
+
export interface SelfServeConfig {
|
|
16
|
+
/** True only when SHIPWRIGHT_SELF_SERVE_ENABLED === "enabled". */
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** Default per-account agent quota (integer >= 0). */
|
|
19
|
+
defaultMaxAgents: number;
|
|
20
|
+
/** Contact address shown when a user needs more quota. */
|
|
21
|
+
contactEmail: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the self-serve config from env. Invalid DEFAULT_MAX_AGENTS values
|
|
26
|
+
* (negative, non-integer, non-numeric) fall back to 0 and log a warning via
|
|
27
|
+
* the injected `warn` (defaults to console.warn).
|
|
28
|
+
*/
|
|
29
|
+
export function parseSelfServeConfig(
|
|
30
|
+
env: SelfServeEnv,
|
|
31
|
+
warn: (message: string) => void = console.warn,
|
|
32
|
+
): SelfServeConfig {
|
|
33
|
+
const enabled = env.SHIPWRIGHT_SELF_SERVE_ENABLED === "enabled";
|
|
34
|
+
|
|
35
|
+
let defaultMaxAgents = 0;
|
|
36
|
+
const raw = env.SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS?.trim();
|
|
37
|
+
if (raw) {
|
|
38
|
+
if (/^\d+$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
39
|
+
defaultMaxAgents = Number(raw);
|
|
40
|
+
} else {
|
|
41
|
+
warn(
|
|
42
|
+
`[admin] invalid SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS=${JSON.stringify(raw)} (expected integer >= 0) — falling back to 0`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const contactEmail =
|
|
48
|
+
env.SHIPWRIGHT_SELF_SERVE_CONTACT_EMAIL?.trim() ||
|
|
49
|
+
DEFAULT_SELF_SERVE_CONTACT_EMAIL;
|
|
50
|
+
|
|
51
|
+
return { enabled, defaultMaxAgents, contactEmail };
|
|
52
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/self-serve-config.unit.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for the self-serve env config parser. Pure logic over an
|
|
5
|
+
* injected env object — no process.env reads, no I/O.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "bun:test";
|
|
9
|
+
import { parseSelfServeConfig } from "./self-serve-config.ts";
|
|
10
|
+
|
|
11
|
+
function parse(env: Record<string, string | undefined>) {
|
|
12
|
+
const warnings: string[] = [];
|
|
13
|
+
const config = parseSelfServeConfig(env, (m) => warnings.push(m));
|
|
14
|
+
return { config, warnings };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("parseSelfServeConfig", () => {
|
|
18
|
+
it("defaults to off, 0 agents, and the default contact when env is empty", () => {
|
|
19
|
+
const { config, warnings } = parse({});
|
|
20
|
+
expect(config).toEqual({
|
|
21
|
+
enabled: false,
|
|
22
|
+
defaultMaxAgents: 0,
|
|
23
|
+
contactEmail: "dan@app-vitals.com",
|
|
24
|
+
});
|
|
25
|
+
expect(warnings).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("enables only on the exact value 'enabled'", () => {
|
|
29
|
+
expect(
|
|
30
|
+
parse({ SHIPWRIGHT_SELF_SERVE_ENABLED: "enabled" }).config.enabled,
|
|
31
|
+
).toBe(true);
|
|
32
|
+
for (const v of ["true", "1", "ENABLED", "", "disabled"]) {
|
|
33
|
+
expect(parse({ SHIPWRIGHT_SELF_SERVE_ENABLED: v }).config.enabled).toBe(
|
|
34
|
+
false,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("parses a valid DEFAULT_MAX_AGENTS without warning", () => {
|
|
40
|
+
const { config, warnings } = parse({
|
|
41
|
+
SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS: "3",
|
|
42
|
+
});
|
|
43
|
+
expect(config.defaultMaxAgents).toBe(3);
|
|
44
|
+
expect(warnings).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("accepts an explicit 0", () => {
|
|
48
|
+
const { config, warnings } = parse({
|
|
49
|
+
SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS: "0",
|
|
50
|
+
});
|
|
51
|
+
expect(config.defaultMaxAgents).toBe(0);
|
|
52
|
+
expect(warnings).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.each(["-1", "1.5", "abc", "1e3", "99999999999999999999"])(
|
|
56
|
+
"falls back to 0 and warns for invalid DEFAULT_MAX_AGENTS %p",
|
|
57
|
+
(value) => {
|
|
58
|
+
const { config, warnings } = parse({
|
|
59
|
+
SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS: value,
|
|
60
|
+
});
|
|
61
|
+
expect(config.defaultMaxAgents).toBe(0);
|
|
62
|
+
expect(warnings).toHaveLength(1);
|
|
63
|
+
expect(warnings[0]).toContain("SHIPWRIGHT_SELF_SERVE_DEFAULT_MAX_AGENTS");
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it("uses a custom contact email, trimmed", () => {
|
|
68
|
+
expect(
|
|
69
|
+
parse({ SHIPWRIGHT_SELF_SERVE_CONTACT_EMAIL: " ops@example.com " }).config
|
|
70
|
+
.contactEmail,
|
|
71
|
+
).toBe("ops@example.com");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("falls back to the default contact when blank", () => {
|
|
75
|
+
expect(
|
|
76
|
+
parse({ SHIPWRIGHT_SELF_SERVE_CONTACT_EMAIL: " " }).config.contactEmail,
|
|
77
|
+
).toBe("dan@app-vitals.com");
|
|
78
|
+
});
|
|
79
|
+
});
|