@app-vitals/agent-admin 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +64 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/trial-expiry-sweeper.unit.test.ts
|
|
3
|
+
* Unit tests for TrialExpirySweeper orchestration and its interval resolver
|
|
4
|
+
* (ATE-3.1).
|
|
5
|
+
*
|
|
6
|
+
* Sibling of session-alert-sweeper.unit.test.ts: everything the sweeper
|
|
7
|
+
* touches is injected — an in-memory AgentCronJobService double, a recording
|
|
8
|
+
* line logger, and a FixedClock. No mock.module(), no global overrides, no
|
|
9
|
+
* real Prisma, no Postgres — per the repo's hard test-isolation rule
|
|
10
|
+
* (CLAUDE.md). The real join query and write path are covered separately in
|
|
11
|
+
* trial-expiry-sweeper.integration.test.ts, which skips entirely unless
|
|
12
|
+
* DATABASE_URL_ADMIN_TEST is set; this file is what runs on every `task test`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from "bun:test";
|
|
16
|
+
import { FixedClock } from "./clock.ts";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS,
|
|
19
|
+
type TrialExpiryCronJobServiceLike,
|
|
20
|
+
TrialExpirySweeper,
|
|
21
|
+
resolveTrialExpirySweepIntervalMs,
|
|
22
|
+
} from "./trial-expiry-sweeper.ts";
|
|
23
|
+
|
|
24
|
+
const NOW = new Date("2026-06-15T00:00:00.000Z");
|
|
25
|
+
|
|
26
|
+
interface FakeRow {
|
|
27
|
+
id: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SetEnabledCall {
|
|
33
|
+
agentId: string;
|
|
34
|
+
cronId: string;
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
lockdown?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-memory stand-in for AgentCronJobService. `listEnabledWithExpiredTrial`
|
|
41
|
+
* returns only rows still flagged enabled, so a second tick naturally sees
|
|
42
|
+
* less work — the same shape the real join query has, which is what makes the
|
|
43
|
+
* sweeper idempotent without any dedup state.
|
|
44
|
+
*/
|
|
45
|
+
function fakeCronJobService(
|
|
46
|
+
seed: FakeRow[] = [],
|
|
47
|
+
opts: {
|
|
48
|
+
failListing?: boolean;
|
|
49
|
+
failSetEnabledFor?: (cronId: string) => boolean;
|
|
50
|
+
} = {},
|
|
51
|
+
): {
|
|
52
|
+
service: TrialExpiryCronJobServiceLike;
|
|
53
|
+
rows: FakeRow[];
|
|
54
|
+
listedNow: Date[];
|
|
55
|
+
calls: SetEnabledCall[];
|
|
56
|
+
} {
|
|
57
|
+
const rows = seed.map((r) => ({ ...r }));
|
|
58
|
+
const listedNow: Date[] = [];
|
|
59
|
+
const calls: SetEnabledCall[] = [];
|
|
60
|
+
|
|
61
|
+
const service: TrialExpiryCronJobServiceLike = {
|
|
62
|
+
listEnabledWithExpiredTrial: async (now: Date) => {
|
|
63
|
+
listedNow.push(now);
|
|
64
|
+
if (opts.failListing) throw new Error("db down");
|
|
65
|
+
return rows
|
|
66
|
+
.filter((r) => r.enabled)
|
|
67
|
+
.map((r) => ({ id: r.id, agentId: r.agentId }));
|
|
68
|
+
},
|
|
69
|
+
setEnabled: async (
|
|
70
|
+
agentId: string,
|
|
71
|
+
cronId: string,
|
|
72
|
+
enabled: boolean,
|
|
73
|
+
setOpts?: { lockdown?: boolean },
|
|
74
|
+
) => {
|
|
75
|
+
calls.push({ agentId, cronId, enabled, lockdown: setOpts?.lockdown });
|
|
76
|
+
if (opts.failSetEnabledFor?.(cronId)) throw new Error("boom");
|
|
77
|
+
const row = rows.find((r) => r.id === cronId);
|
|
78
|
+
if (!row) throw new Error(`no such cron ${cronId}`);
|
|
79
|
+
row.enabled = enabled;
|
|
80
|
+
return row;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return { service, rows, listedNow, calls };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sweeperFor(
|
|
88
|
+
service: TrialExpiryCronJobServiceLike,
|
|
89
|
+
lines: string[] = [],
|
|
90
|
+
): TrialExpirySweeper {
|
|
91
|
+
return new TrialExpirySweeper({
|
|
92
|
+
agentCronJobService: service,
|
|
93
|
+
clock: FixedClock(NOW),
|
|
94
|
+
log: (line) => lines.push(line),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── resolveTrialExpirySweepIntervalMs ──────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
// The pure env rule behind the trial-expiry sweeper's setInterval cadence in
|
|
101
|
+
// startServer(). Tested without touching process.env.
|
|
102
|
+
describe("resolveTrialExpirySweepIntervalMs", () => {
|
|
103
|
+
it("defaults to 60s when unset", () => {
|
|
104
|
+
expect(resolveTrialExpirySweepIntervalMs({})).toBe(
|
|
105
|
+
DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS,
|
|
106
|
+
);
|
|
107
|
+
expect(DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS).toBe(60_000);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("uses an explicit positive override", () => {
|
|
111
|
+
expect(
|
|
112
|
+
resolveTrialExpirySweepIntervalMs({
|
|
113
|
+
SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS: "5000",
|
|
114
|
+
}),
|
|
115
|
+
).toBe(5000);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("trims surrounding whitespace around a valid value", () => {
|
|
119
|
+
expect(
|
|
120
|
+
resolveTrialExpirySweepIntervalMs({
|
|
121
|
+
SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS: " 1500 ",
|
|
122
|
+
}),
|
|
123
|
+
).toBe(1500);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("falls back to the default for blank, non-numeric, zero, or negative values", () => {
|
|
127
|
+
// A 0/NaN interval would make setInterval spin; a negative one is
|
|
128
|
+
// nonsense. Both must resolve to the default rather than reach setInterval.
|
|
129
|
+
for (const raw of ["", " ", "soon", "NaN", "0", "-1", "-5000"]) {
|
|
130
|
+
expect(
|
|
131
|
+
resolveTrialExpirySweepIntervalMs({
|
|
132
|
+
SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS: raw,
|
|
133
|
+
}),
|
|
134
|
+
).toBe(DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("falls back to the default for Infinity", () => {
|
|
139
|
+
expect(
|
|
140
|
+
resolveTrialExpirySweepIntervalMs({
|
|
141
|
+
SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS: "Infinity",
|
|
142
|
+
}),
|
|
143
|
+
).toBe(DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ignores unrelated env vars", () => {
|
|
147
|
+
expect(
|
|
148
|
+
resolveTrialExpirySweepIntervalMs({
|
|
149
|
+
SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS: "1234",
|
|
150
|
+
}),
|
|
151
|
+
).toBe(DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── tick(): the happy path ─────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
describe("TrialExpirySweeper.tick — disabling expired-trial crons", () => {
|
|
158
|
+
it("disables every listed row and counts them", async () => {
|
|
159
|
+
const { service, rows, calls } = fakeCronJobService([
|
|
160
|
+
{ id: "cron_a", agentId: "agt_1", enabled: true },
|
|
161
|
+
{ id: "cron_b", agentId: "agt_1", enabled: true },
|
|
162
|
+
{ id: "cron_c", agentId: "agt_2", enabled: true },
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
const result = await sweeperFor(service).tick();
|
|
166
|
+
|
|
167
|
+
expect(result).toEqual({ disabled: 3 });
|
|
168
|
+
expect(rows.every((r) => r.enabled === false)).toBe(true);
|
|
169
|
+
expect(calls).toEqual([
|
|
170
|
+
{ agentId: "agt_1", cronId: "cron_a", enabled: false, lockdown: true },
|
|
171
|
+
{ agentId: "agt_1", cronId: "cron_b", enabled: false, lockdown: true },
|
|
172
|
+
{ agentId: "agt_2", cronId: "cron_c", enabled: false, lockdown: true },
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("passes the injected clock's time through to the listing query", async () => {
|
|
177
|
+
const { service, listedNow } = fakeCronJobService([]);
|
|
178
|
+
|
|
179
|
+
await sweeperFor(service).tick();
|
|
180
|
+
|
|
181
|
+
expect(listedNow).toHaveLength(1);
|
|
182
|
+
expect(listedNow[0]?.toISOString()).toBe(NOW.toISOString());
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("no-ops cleanly when nothing is listed", async () => {
|
|
186
|
+
const lines: string[] = [];
|
|
187
|
+
const { service, calls } = fakeCronJobService([]);
|
|
188
|
+
|
|
189
|
+
const result = await sweeperFor(service, lines).tick();
|
|
190
|
+
|
|
191
|
+
expect(result).toEqual({ disabled: 0 });
|
|
192
|
+
expect(calls).toHaveLength(0);
|
|
193
|
+
// Nothing disabled → no log line (an idle sweeper must stay quiet; it
|
|
194
|
+
// ticks once a minute forever).
|
|
195
|
+
expect(lines).toHaveLength(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("logs a single count line only when it disabled something", async () => {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
const { service } = fakeCronJobService([
|
|
201
|
+
{ id: "cron_a", agentId: "agt_1", enabled: true },
|
|
202
|
+
{ id: "cron_b", agentId: "agt_1", enabled: true },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
const sweeper = sweeperFor(service, lines);
|
|
206
|
+
await sweeper.tick();
|
|
207
|
+
expect(lines).toEqual(["[trial-expiry-sweeper] disabled=2"]);
|
|
208
|
+
|
|
209
|
+
// Second tick has nothing left → no further lines.
|
|
210
|
+
await sweeper.tick();
|
|
211
|
+
expect(lines).toHaveLength(1);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ─── Idempotency ────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
// The sweeper carries no "have I already swept this agent" state (unlike
|
|
218
|
+
// session-alert-sweeper's SessionAlertState dedup): once its crons are
|
|
219
|
+
// disabled the listing simply stops returning them.
|
|
220
|
+
describe("TrialExpirySweeper.tick — idempotency", () => {
|
|
221
|
+
it("finds nothing left to do on a second tick", async () => {
|
|
222
|
+
const { service, calls } = fakeCronJobService([
|
|
223
|
+
{ id: "cron_a", agentId: "agt_1", enabled: true },
|
|
224
|
+
]);
|
|
225
|
+
const sweeper = sweeperFor(service);
|
|
226
|
+
|
|
227
|
+
expect(await sweeper.tick()).toEqual({ disabled: 1 });
|
|
228
|
+
expect(await sweeper.tick()).toEqual({ disabled: 0 });
|
|
229
|
+
expect(await sweeper.tick()).toEqual({ disabled: 0 });
|
|
230
|
+
expect(calls).toHaveLength(1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("never re-counts an already-disabled row the listing excludes", async () => {
|
|
234
|
+
const { service, calls } = fakeCronJobService([
|
|
235
|
+
{ id: "cron_on", agentId: "agt_1", enabled: true },
|
|
236
|
+
{ id: "cron_off", agentId: "agt_1", enabled: false },
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const result = await sweeperFor(service).tick();
|
|
240
|
+
|
|
241
|
+
expect(result).toEqual({ disabled: 1 });
|
|
242
|
+
expect(calls.map((c) => c.cronId)).toEqual(["cron_on"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("is safe under overlapping ticks — no re-entrancy guard needed", async () => {
|
|
246
|
+
// Two concurrent sweeps both disabling the same row is harmless because
|
|
247
|
+
// setEnabled is idempotent; the sweeper deliberately has no in-flight
|
|
248
|
+
// guard (unlike the session sweeper, whose pushes have side effects).
|
|
249
|
+
const { service, rows } = fakeCronJobService([
|
|
250
|
+
{ id: "cron_a", agentId: "agt_1", enabled: true },
|
|
251
|
+
]);
|
|
252
|
+
const sweeper = sweeperFor(service);
|
|
253
|
+
|
|
254
|
+
const [first, second] = await Promise.all([sweeper.tick(), sweeper.tick()]);
|
|
255
|
+
|
|
256
|
+
expect(first.disabled + second.disabled).toBeGreaterThanOrEqual(1);
|
|
257
|
+
expect(rows[0]?.enabled).toBe(false);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ─── Resilience: one bad row never aborts the sweep ─────────────────────────
|
|
262
|
+
|
|
263
|
+
describe("TrialExpirySweeper.tick — per-row resilience", () => {
|
|
264
|
+
it("keeps sweeping after one row's setEnabled throws", async () => {
|
|
265
|
+
const { service, rows, calls } = fakeCronJobService(
|
|
266
|
+
[
|
|
267
|
+
{ id: "cron_bad", agentId: "agt_1", enabled: true },
|
|
268
|
+
{ id: "cron_good", agentId: "agt_1", enabled: true },
|
|
269
|
+
{ id: "cron_also_good", agentId: "agt_2", enabled: true },
|
|
270
|
+
],
|
|
271
|
+
{ failSetEnabledFor: (cronId) => cronId === "cron_bad" },
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const result = await sweeperFor(service).tick();
|
|
275
|
+
|
|
276
|
+
// The failure is counted out, not thrown out: the two healthy rows are
|
|
277
|
+
// still disabled and every row was attempted.
|
|
278
|
+
expect(result).toEqual({ disabled: 2 });
|
|
279
|
+
expect(calls).toHaveLength(3);
|
|
280
|
+
expect(rows.find((r) => r.id === "cron_bad")?.enabled).toBe(true);
|
|
281
|
+
expect(rows.find((r) => r.id === "cron_good")?.enabled).toBe(false);
|
|
282
|
+
expect(rows.find((r) => r.id === "cron_also_good")?.enabled).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("retries the failed row on the next tick", async () => {
|
|
286
|
+
// Because the failed row stays enabled, the next listing returns it again
|
|
287
|
+
// — a transient write failure self-heals rather than leaking a cron that
|
|
288
|
+
// outlives the trial.
|
|
289
|
+
let fail = true;
|
|
290
|
+
const { service, rows } = fakeCronJobService(
|
|
291
|
+
[
|
|
292
|
+
{ id: "cron_flaky", agentId: "agt_1", enabled: true },
|
|
293
|
+
{ id: "cron_good", agentId: "agt_1", enabled: true },
|
|
294
|
+
],
|
|
295
|
+
{ failSetEnabledFor: (cronId) => fail && cronId === "cron_flaky" },
|
|
296
|
+
);
|
|
297
|
+
const sweeper = sweeperFor(service);
|
|
298
|
+
|
|
299
|
+
expect(await sweeper.tick()).toEqual({ disabled: 1 });
|
|
300
|
+
|
|
301
|
+
fail = false;
|
|
302
|
+
expect(await sweeper.tick()).toEqual({ disabled: 1 });
|
|
303
|
+
expect(rows.every((r) => r.enabled === false)).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns disabled=0 (and never throws) when the listing fails", async () => {
|
|
307
|
+
const lines: string[] = [];
|
|
308
|
+
const { service, calls } = fakeCronJobService(
|
|
309
|
+
[{ id: "cron_a", agentId: "agt_1", enabled: true }],
|
|
310
|
+
{ failListing: true },
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Fail-open: a DB blip must not crash the setInterval callback in main.ts.
|
|
314
|
+
const result = await sweeperFor(service, lines).tick();
|
|
315
|
+
|
|
316
|
+
expect(result).toEqual({ disabled: 0 });
|
|
317
|
+
expect(calls).toHaveLength(0);
|
|
318
|
+
expect(lines).toHaveLength(0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("recovers on the next tick after a listing failure", async () => {
|
|
322
|
+
const rows: FakeRow[] = [{ id: "cron_a", agentId: "agt_1", enabled: true }];
|
|
323
|
+
let broken = true;
|
|
324
|
+
const service: TrialExpiryCronJobServiceLike = {
|
|
325
|
+
listEnabledWithExpiredTrial: async () => {
|
|
326
|
+
if (broken) throw new Error("db down");
|
|
327
|
+
return rows
|
|
328
|
+
.filter((r) => r.enabled)
|
|
329
|
+
.map((r) => ({ id: r.id, agentId: r.agentId }));
|
|
330
|
+
},
|
|
331
|
+
setEnabled: async (_agentId, cronId, enabled) => {
|
|
332
|
+
const row = rows.find((r) => r.id === cronId);
|
|
333
|
+
if (row) row.enabled = enabled;
|
|
334
|
+
return row;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
const sweeper = sweeperFor(service);
|
|
338
|
+
|
|
339
|
+
expect(await sweeper.tick()).toEqual({ disabled: 0 });
|
|
340
|
+
|
|
341
|
+
broken = false;
|
|
342
|
+
expect(await sweeper.tick()).toEqual({ disabled: 1 });
|
|
343
|
+
expect(rows[0]?.enabled).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ─── Deliberate non-behavior ────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
// A trial ending must never destroy the workspace — the sweeper only flips
|
|
350
|
+
// AgentCronJob.enabled. The narrow injected interface is the guard rail: if
|
|
351
|
+
// deprovisioning were ever added, this test's double would have to grow a
|
|
352
|
+
// method to satisfy it.
|
|
353
|
+
describe("TrialExpirySweeper.tick — scope", () => {
|
|
354
|
+
it("only ever calls setEnabled(..., false) — no other service method", async () => {
|
|
355
|
+
const seen: string[] = [];
|
|
356
|
+
const base = fakeCronJobService([
|
|
357
|
+
{ id: "cron_a", agentId: "agt_1", enabled: true },
|
|
358
|
+
{ id: "cron_b", agentId: "agt_2", enabled: true },
|
|
359
|
+
]);
|
|
360
|
+
// A double that also fails loudly on anything deprovision-shaped: if the
|
|
361
|
+
// sweeper ever grew a deleteAgentFully()-style call, it would land here.
|
|
362
|
+
const recording: TrialExpiryCronJobServiceLike & Record<string, unknown> = {
|
|
363
|
+
listEnabledWithExpiredTrial: (now: Date) => {
|
|
364
|
+
seen.push("listEnabledWithExpiredTrial");
|
|
365
|
+
return base.service.listEnabledWithExpiredTrial(now);
|
|
366
|
+
},
|
|
367
|
+
setEnabled: (
|
|
368
|
+
agentId: string,
|
|
369
|
+
cronId: string,
|
|
370
|
+
enabled: boolean,
|
|
371
|
+
opts?: { lockdown?: boolean },
|
|
372
|
+
) => {
|
|
373
|
+
seen.push("setEnabled");
|
|
374
|
+
return base.service.setEnabled(agentId, cronId, enabled, opts);
|
|
375
|
+
},
|
|
376
|
+
deleteAgentFully: () => {
|
|
377
|
+
throw new Error("sweeper must never deprovision an agent");
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
await sweeperFor(recording).tick();
|
|
382
|
+
|
|
383
|
+
expect([...new Set(seen)].sort()).toEqual([
|
|
384
|
+
"listEnabledWithExpiredTrial",
|
|
385
|
+
"setEnabled",
|
|
386
|
+
]);
|
|
387
|
+
expect(base.calls.every((c) => c.enabled === false)).toBe(true);
|
|
388
|
+
expect(base.calls.every((c) => c.lockdown === true)).toBe(true);
|
|
389
|
+
expect(base.rows.every((r) => r.enabled === false)).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
});
|