@app-vitals/agent-admin 0.0.1 → 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 +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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/account-trial-expiry-sweeper.ts
|
|
3
|
+
*
|
|
4
|
+
* AccountTrialExpirySweeper (SSP-8.2) — the account-level counterpart of
|
|
5
|
+
* trial-expiry-sweeper.ts (which keeps handling per-agent trialExpiresAt,
|
|
6
|
+
* unchanged). Every tick:
|
|
7
|
+
*
|
|
8
|
+
* 1. Flips every *active* account whose trialExpiresAt is strictly in the
|
|
9
|
+
* past to 'trial_expired' (conditional write — a concurrent admin
|
|
10
|
+
* reactivation/extension wins).
|
|
11
|
+
* 2. Runs AccountLifecycle.lockDown() across all locked accounts
|
|
12
|
+
* (suspended or trial_expired): every enabled cron of their agents is
|
|
13
|
+
* disabled with the SSP-8.1 lockdown marker. This both locks down the
|
|
14
|
+
* accounts expired in step 1 and self-heals any cron re-enabled (or
|
|
15
|
+
* created) while its account is locked — mirroring how the per-agent
|
|
16
|
+
* sweeper continuously re-applies its lockdown.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent by construction: once an account is trial_expired and its crons
|
|
19
|
+
* are disabled, neither query matches anything, so a re-run writes nothing.
|
|
20
|
+
* Never deletes anything (no deleteAgentFully call). Registered via setInterval
|
|
21
|
+
* in main.ts, flag-gated on self-serve.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { AccountLifecycle } from "./account-lifecycle.ts";
|
|
25
|
+
import type { AccountService } from "./accounts.ts";
|
|
26
|
+
import { type Clock, SystemClock } from "./clock.ts";
|
|
27
|
+
|
|
28
|
+
interface AccountTrialExpirySweeperDeps {
|
|
29
|
+
accounts: Pick<AccountService, "listExpiredActiveIds" | "expireTrial">;
|
|
30
|
+
lifecycle: Pick<AccountLifecycle, "lockDown">;
|
|
31
|
+
clock?: Clock;
|
|
32
|
+
log?: (line: string) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AccountTrialExpirySweepResult {
|
|
36
|
+
/** Accounts flipped active → trial_expired this tick. */
|
|
37
|
+
expired: number;
|
|
38
|
+
/** Crons disabled (lockdown-marked) this tick. */
|
|
39
|
+
disabled: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class AccountTrialExpirySweeper {
|
|
43
|
+
private readonly clock: Clock;
|
|
44
|
+
private readonly log: (line: string) => void;
|
|
45
|
+
|
|
46
|
+
constructor(private readonly deps: AccountTrialExpirySweeperDeps) {
|
|
47
|
+
this.clock = deps.clock ?? SystemClock();
|
|
48
|
+
this.log = deps.log ?? ((line) => console.log(line));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One sweep. Never throws; per-account failures are logged and skipped. */
|
|
52
|
+
async tick(): Promise<AccountTrialExpirySweepResult> {
|
|
53
|
+
const now = this.clock.now();
|
|
54
|
+
let expired = 0;
|
|
55
|
+
try {
|
|
56
|
+
const ids = await this.deps.accounts.listExpiredActiveIds(now);
|
|
57
|
+
for (const id of ids) {
|
|
58
|
+
try {
|
|
59
|
+
if (await this.deps.accounts.expireTrial(id, now)) expired++;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(
|
|
62
|
+
`[account-trial-expiry-sweeper] expire account ${id} failed:`,
|
|
63
|
+
err,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error("[account-trial-expiry-sweeper] listing failed:", err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let disabled = 0;
|
|
72
|
+
try {
|
|
73
|
+
disabled = await this.deps.lifecycle.lockDown();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error("[account-trial-expiry-sweeper] lockdown failed:", err);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (expired > 0 || disabled > 0) {
|
|
79
|
+
this.log(
|
|
80
|
+
`[account-trial-expiry-sweeper] expired=${expired} disabled=${disabled}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return { expired, disabled };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/account-trial-warning-sweeper.integration.test.ts
|
|
3
|
+
* AccountTrialWarningSweeper (SSP-8.2) against real Postgres fixture accounts
|
|
4
|
+
* and agents, with an injected AgentEnvService double (per-agent Slack
|
|
5
|
+
* config), an injected Slack-send double and a FixedClock. No real Slack.
|
|
6
|
+
*
|
|
7
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
11
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
12
|
+
import {
|
|
13
|
+
AccountTrialWarningSweeper,
|
|
14
|
+
type AccountTrialWarningSweeperDeps,
|
|
15
|
+
} from "./account-trial-warning-sweeper.ts";
|
|
16
|
+
import { AccountService } from "./accounts.ts";
|
|
17
|
+
import { FixedClock } from "./clock.ts";
|
|
18
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
19
|
+
import type { SendSlackWarningParams } from "./trial-expiry-warning-sweeper.ts";
|
|
20
|
+
|
|
21
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
22
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
23
|
+
|
|
24
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
25
|
+
const NOW = new Date("2026-10-08T12:00:00.000Z");
|
|
26
|
+
const days = (n: number) => new Date(NOW.getTime() + n * DAY);
|
|
27
|
+
|
|
28
|
+
describeOrSkip("AccountTrialWarningSweeper (integration)", () => {
|
|
29
|
+
let prisma: PrismaClient;
|
|
30
|
+
let accounts: AccountService;
|
|
31
|
+
let sends: SendSlackWarningParams[];
|
|
32
|
+
/** agentId → Slack env; absent → no config bundle. */
|
|
33
|
+
let slackEnv: Map<string, Record<string, string>>;
|
|
34
|
+
let sendOk: (p: SendSlackWarningParams) => boolean;
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
prisma = createAdminPrismaClient(TEST_DB as string);
|
|
38
|
+
await prisma.agentToken.deleteMany();
|
|
39
|
+
await prisma.agentCronJob.deleteMany();
|
|
40
|
+
await prisma.agentTool.deleteMany();
|
|
41
|
+
await prisma.agentEnv.deleteMany();
|
|
42
|
+
await prisma.agent.deleteMany();
|
|
43
|
+
await prisma.accountInvite.deleteMany();
|
|
44
|
+
await prisma.accountMember.deleteMany();
|
|
45
|
+
await prisma.account.deleteMany();
|
|
46
|
+
accounts = new AccountService(prisma);
|
|
47
|
+
sends = [];
|
|
48
|
+
slackEnv = new Map();
|
|
49
|
+
sendOk = () => true;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await prisma.$disconnect();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function makeSweeper(
|
|
57
|
+
overrides: Partial<AccountTrialWarningSweeperDeps> = {},
|
|
58
|
+
): AccountTrialWarningSweeper {
|
|
59
|
+
return new AccountTrialWarningSweeper({
|
|
60
|
+
accounts,
|
|
61
|
+
agentEnvService: {
|
|
62
|
+
getConfigBundle: async (agentId) => {
|
|
63
|
+
const env = slackEnv.get(agentId);
|
|
64
|
+
return env ? { env, agentId, allowedTools: [] } : null;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
sendSlackMessage: async (p) => {
|
|
68
|
+
sends.push(p);
|
|
69
|
+
return sendOk(p);
|
|
70
|
+
},
|
|
71
|
+
clock: FixedClock(NOW),
|
|
72
|
+
warningDays: 3,
|
|
73
|
+
log: () => {},
|
|
74
|
+
...overrides,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function makeAccount(
|
|
79
|
+
opts: {
|
|
80
|
+
name?: string;
|
|
81
|
+
status?: string;
|
|
82
|
+
trialExpiresAt?: Date | null;
|
|
83
|
+
trialExpiryWarnedAt?: Date | null;
|
|
84
|
+
} = {},
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
const a = await prisma.account.create({
|
|
87
|
+
data: {
|
|
88
|
+
name: opts.name ?? "Acme",
|
|
89
|
+
status: opts.status ?? "active",
|
|
90
|
+
trialExpiresAt: opts.trialExpiresAt ?? null,
|
|
91
|
+
trialExpiryWarnedAt: opts.trialExpiryWarnedAt ?? null,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return a.id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function makeAgent(
|
|
98
|
+
accountId: string | null,
|
|
99
|
+
slack = true,
|
|
100
|
+
name = "bot",
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const a = await prisma.agent.create({
|
|
103
|
+
data: { name, typeName: "coding", accountId },
|
|
104
|
+
});
|
|
105
|
+
if (slack) {
|
|
106
|
+
slackEnv.set(a.id, {
|
|
107
|
+
SLACK_BOT_TOKEN: `xoxb-${a.id}`,
|
|
108
|
+
SLACK_ALERT_CHANNEL: `#alerts-${a.id}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return a.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const warnedAt = async (id: string) =>
|
|
115
|
+
(await prisma.account.findUniqueOrThrow({ where: { id } }))
|
|
116
|
+
.trialExpiryWarnedAt;
|
|
117
|
+
|
|
118
|
+
it("2 days away with a 3-day window -> exactly one warning across repeated sweeps", async () => {
|
|
119
|
+
const acc = await makeAccount({
|
|
120
|
+
name: "Acme Co",
|
|
121
|
+
trialExpiresAt: days(2),
|
|
122
|
+
});
|
|
123
|
+
const agent = await makeAgent(acc);
|
|
124
|
+
const sweeper = makeSweeper();
|
|
125
|
+
|
|
126
|
+
const first = await sweeper.tick();
|
|
127
|
+
expect(first.warned).toBe(1);
|
|
128
|
+
expect(sends).toHaveLength(1);
|
|
129
|
+
expect(sends[0]).toMatchObject({
|
|
130
|
+
botToken: `xoxb-${agent}`,
|
|
131
|
+
channel: `#alerts-${agent}`,
|
|
132
|
+
});
|
|
133
|
+
expect(sends[0].text).toContain("Acme Co");
|
|
134
|
+
expect(sends[0].text).toContain(days(2).toISOString().slice(0, 10));
|
|
135
|
+
expect(await warnedAt(acc)).toEqual(NOW);
|
|
136
|
+
|
|
137
|
+
for (let i = 0; i < 3; i++) {
|
|
138
|
+
expect((await sweeper.tick()).warned).toBe(0);
|
|
139
|
+
}
|
|
140
|
+
expect(sends).toHaveLength(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("posts once via each of the account's agents' own alert channel", async () => {
|
|
144
|
+
const acc = await makeAccount({ trialExpiresAt: days(2) });
|
|
145
|
+
const a1 = await makeAgent(acc, true, "one");
|
|
146
|
+
const a2 = await makeAgent(acc, true, "two");
|
|
147
|
+
await makeAgent(null); // unrelated agent, never posted to
|
|
148
|
+
|
|
149
|
+
const result = await makeSweeper().tick();
|
|
150
|
+
expect(result.warned).toBe(1);
|
|
151
|
+
expect(sends.map((s) => s.channel).sort()).toEqual(
|
|
152
|
+
[`#alerts-${a1}`, `#alerts-${a2}`].sort(),
|
|
153
|
+
);
|
|
154
|
+
await makeSweeper().tick();
|
|
155
|
+
expect(sends).toHaveLength(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("not yet in the window -> no warning, no stamp", async () => {
|
|
159
|
+
const acc = await makeAccount({ trialExpiresAt: days(5) });
|
|
160
|
+
await makeAgent(acc);
|
|
161
|
+
expect((await makeSweeper().tick()).warned).toBe(0);
|
|
162
|
+
expect(sends).toHaveLength(0);
|
|
163
|
+
expect(await warnedAt(acc)).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("no agent has Slack config -> skipped, not stamped, retried next tick", async () => {
|
|
167
|
+
const acc = await makeAccount({ trialExpiresAt: days(2) });
|
|
168
|
+
const agent = await makeAgent(acc, false);
|
|
169
|
+
const sweeper = makeSweeper();
|
|
170
|
+
|
|
171
|
+
expect(await sweeper.tick()).toMatchObject({ warned: 0, skipped: 1 });
|
|
172
|
+
expect(await warnedAt(acc)).toBeNull();
|
|
173
|
+
|
|
174
|
+
slackEnv.set(agent, {
|
|
175
|
+
SLACK_BOT_TOKEN: "xoxb-late",
|
|
176
|
+
SLACK_ALERT_CHANNEL: "#late",
|
|
177
|
+
});
|
|
178
|
+
expect((await sweeper.tick()).warned).toBe(1);
|
|
179
|
+
expect(sends).toHaveLength(1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("every send fails -> failed, not stamped", async () => {
|
|
183
|
+
const acc = await makeAccount({ trialExpiresAt: days(2) });
|
|
184
|
+
await makeAgent(acc);
|
|
185
|
+
sendOk = () => false;
|
|
186
|
+
expect(await makeSweeper().tick()).toMatchObject({ warned: 0, failed: 1 });
|
|
187
|
+
expect(await warnedAt(acc)).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("account with no agents -> skipped, not stamped", async () => {
|
|
191
|
+
const acc = await makeAccount({ trialExpiresAt: days(2) });
|
|
192
|
+
expect(await makeSweeper().tick()).toMatchObject({ skipped: 1 });
|
|
193
|
+
expect(await warnedAt(acc)).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("never warns suspended/expired accounts, already-warned accounts, or long-lapsed trials", async () => {
|
|
197
|
+
await makeAgent(
|
|
198
|
+
await makeAccount({ status: "suspended", trialExpiresAt: days(2) }),
|
|
199
|
+
);
|
|
200
|
+
await makeAgent(
|
|
201
|
+
await makeAccount({ trialExpiresAt: days(2), trialExpiryWarnedAt: NOW }),
|
|
202
|
+
);
|
|
203
|
+
const stale = await makeAccount({ trialExpiresAt: days(-30) });
|
|
204
|
+
await makeAgent(stale);
|
|
205
|
+
|
|
206
|
+
const result = await makeSweeper().tick();
|
|
207
|
+
expect(result).toMatchObject({ warned: 0, stale: 1 });
|
|
208
|
+
expect(sends).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("a candidate fetch failure returns all zeros without throwing", async () => {
|
|
212
|
+
const sweeper = makeSweeper({
|
|
213
|
+
accounts: {
|
|
214
|
+
listTrialWarningCandidates: async () => {
|
|
215
|
+
throw new Error("db down");
|
|
216
|
+
},
|
|
217
|
+
listAgents: accounts.listAgents.bind(accounts),
|
|
218
|
+
markTrialWarned: accounts.markTrialWarned.bind(accounts),
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
expect(await sweeper.tick()).toEqual({
|
|
222
|
+
warned: 0,
|
|
223
|
+
skipped: 0,
|
|
224
|
+
failed: 0,
|
|
225
|
+
stale: 0,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/account-trial-warning-sweeper.ts
|
|
3
|
+
*
|
|
4
|
+
* AccountTrialWarningSweeper (SSP-8.2) — the account-level counterpart of
|
|
5
|
+
* trial-expiry-warning-sweeper.ts (which keeps warning per-agent trials,
|
|
6
|
+
* unchanged). Every tick:
|
|
7
|
+
*
|
|
8
|
+
* 1. Lists active accounts with trialExpiresAt set and trialExpiryWarnedAt
|
|
9
|
+
* null.
|
|
10
|
+
* 2. For each whose trialExpiresAt is inside the warning band
|
|
11
|
+
* (isAccountTrialWarningDue — same window/grace semantics as the
|
|
12
|
+
* per-agent sweeper, window from resolveTrialExpiryWarningDays), posts
|
|
13
|
+
* one Slack warning via *each* of the account's agents' own
|
|
14
|
+
* SLACK_BOT_TOKEN / SLACK_ALERT_CHANNEL (admin holds no Slack secret of
|
|
15
|
+
* its own — same pattern as the per-agent sweeper).
|
|
16
|
+
* 3. If at least one post went out, stamps trialExpiryWarnedAt so the
|
|
17
|
+
* account is never warned again (until a reactivation/extension resets
|
|
18
|
+
* it, see account-lifecycle.ts). If no agent had usable Slack config,
|
|
19
|
+
* or every send failed, nothing is stamped and the next tick retries.
|
|
20
|
+
*
|
|
21
|
+
* Non-re-entrant (`sweeping` guard), per-account try/catch, injected Clock
|
|
22
|
+
* and Slack sender — structure mirrors trial-expiry-warning-sweeper.ts.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { isAccountTrialWarningDue } from "./account-lifecycle.ts";
|
|
26
|
+
import type { AccountService } from "./accounts.ts";
|
|
27
|
+
import type { AgentEnvService } from "./agent-envs.ts";
|
|
28
|
+
import { type Clock, SystemClock } from "./clock.ts";
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_TRIAL_EXPIRY_WARNING_DAYS,
|
|
31
|
+
isPastWarningGrace,
|
|
32
|
+
type SendSlackWarning,
|
|
33
|
+
sendTrialExpiryWarning,
|
|
34
|
+
} from "./trial-expiry-warning-sweeper.ts";
|
|
35
|
+
|
|
36
|
+
export interface AccountTrialWarningSweeperDeps {
|
|
37
|
+
accounts: Pick<
|
|
38
|
+
AccountService,
|
|
39
|
+
"listTrialWarningCandidates" | "listAgents" | "markTrialWarned"
|
|
40
|
+
>;
|
|
41
|
+
agentEnvService: Pick<AgentEnvService, "getConfigBundle">;
|
|
42
|
+
/** Defaults to sendTrialExpiryWarning (real @slack/web-api client). */
|
|
43
|
+
sendSlackMessage?: SendSlackWarning;
|
|
44
|
+
clock?: Clock;
|
|
45
|
+
warningDays?: number;
|
|
46
|
+
graceDays?: number;
|
|
47
|
+
log?: (line: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface AccountTrialWarningSweepResult {
|
|
51
|
+
/** Accounts warned (≥1 post sent) and stamped. */
|
|
52
|
+
warned: number;
|
|
53
|
+
/** Due, but no agent of the account had usable Slack config. */
|
|
54
|
+
skipped: number;
|
|
55
|
+
/** Due, Slack config present, but every send failed. */
|
|
56
|
+
failed: number;
|
|
57
|
+
/** Never warned and trialExpiresAt beyond the grace window — not alerted. */
|
|
58
|
+
stale: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Candidate = Awaited<
|
|
62
|
+
ReturnType<AccountService["listTrialWarningCandidates"]>
|
|
63
|
+
>[number];
|
|
64
|
+
|
|
65
|
+
/** Slack text for one account's warning, posted through one of its agents. */
|
|
66
|
+
function buildAccountTrialWarningMessage(
|
|
67
|
+
accountName: string,
|
|
68
|
+
trialExpiresAt: Date,
|
|
69
|
+
now: Date,
|
|
70
|
+
): string {
|
|
71
|
+
const dateStr = trialExpiresAt.toISOString().slice(0, 10);
|
|
72
|
+
const verb =
|
|
73
|
+
trialExpiresAt.getTime() <= now.getTime() ? "expired on" : "expires on";
|
|
74
|
+
return (
|
|
75
|
+
`:warning: The trial for account \`${accountName}\` ${verb} ${dateStr}. ` +
|
|
76
|
+
`After that, every agent in the account is paused: crons are disabled ` +
|
|
77
|
+
`and Slack access is blocked until the trial is renewed. Nothing is deleted.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const zero = (): AccountTrialWarningSweepResult => ({
|
|
82
|
+
warned: 0,
|
|
83
|
+
skipped: 0,
|
|
84
|
+
failed: 0,
|
|
85
|
+
stale: 0,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export class AccountTrialWarningSweeper {
|
|
89
|
+
private readonly clock: Clock;
|
|
90
|
+
private readonly warningDays: number;
|
|
91
|
+
private readonly graceDays: number;
|
|
92
|
+
private readonly send: SendSlackWarning;
|
|
93
|
+
private readonly log: (line: string) => void;
|
|
94
|
+
private sweeping = false;
|
|
95
|
+
|
|
96
|
+
constructor(private readonly deps: AccountTrialWarningSweeperDeps) {
|
|
97
|
+
this.clock = deps.clock ?? SystemClock();
|
|
98
|
+
this.warningDays = deps.warningDays ?? DEFAULT_TRIAL_EXPIRY_WARNING_DAYS;
|
|
99
|
+
this.graceDays = deps.graceDays ?? this.warningDays;
|
|
100
|
+
this.send = deps.sendSlackMessage ?? sendTrialExpiryWarning;
|
|
101
|
+
this.log = deps.log ?? ((line) => console.log(line));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One sweep. Never throws; skips (all zeros) if a tick is in flight. */
|
|
105
|
+
async tick(): Promise<AccountTrialWarningSweepResult> {
|
|
106
|
+
if (this.sweeping) {
|
|
107
|
+
console.warn(
|
|
108
|
+
"[account-trial-warning-sweeper] previous tick still in flight — skipping",
|
|
109
|
+
);
|
|
110
|
+
return zero();
|
|
111
|
+
}
|
|
112
|
+
this.sweeping = true;
|
|
113
|
+
try {
|
|
114
|
+
return await this.sweep();
|
|
115
|
+
} finally {
|
|
116
|
+
this.sweeping = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async sweep(): Promise<AccountTrialWarningSweepResult> {
|
|
121
|
+
const result = zero();
|
|
122
|
+
let candidates: Candidate[];
|
|
123
|
+
try {
|
|
124
|
+
candidates = await this.deps.accounts.listTrialWarningCandidates();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error(
|
|
127
|
+
"[account-trial-warning-sweeper] candidate fetch failed:",
|
|
128
|
+
err,
|
|
129
|
+
);
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const now = this.clock.now();
|
|
134
|
+
for (const account of candidates) {
|
|
135
|
+
try {
|
|
136
|
+
await this.sweepAccount(account, now, result);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(
|
|
139
|
+
`[account-trial-warning-sweeper] account ${account.id} failed:`,
|
|
140
|
+
err,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.warned + result.skipped + result.failed + result.stale > 0) {
|
|
146
|
+
this.log(
|
|
147
|
+
`[account-trial-warning-sweeper] warned=${result.warned} skipped=${result.skipped} failed=${result.failed} stale=${result.stale}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async sweepAccount(
|
|
154
|
+
account: Candidate,
|
|
155
|
+
now: Date,
|
|
156
|
+
result: AccountTrialWarningSweepResult,
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const { trialExpiresAt } = account;
|
|
159
|
+
if (
|
|
160
|
+
!trialExpiresAt ||
|
|
161
|
+
!isAccountTrialWarningDue(
|
|
162
|
+
trialExpiresAt,
|
|
163
|
+
account.trialExpiryWarnedAt,
|
|
164
|
+
now,
|
|
165
|
+
this.warningDays,
|
|
166
|
+
this.graceDays,
|
|
167
|
+
)
|
|
168
|
+
) {
|
|
169
|
+
if (
|
|
170
|
+
trialExpiresAt &&
|
|
171
|
+
!account.trialExpiryWarnedAt &&
|
|
172
|
+
isPastWarningGrace(trialExpiresAt, now, this.graceDays)
|
|
173
|
+
) {
|
|
174
|
+
result.stale++;
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const text = buildAccountTrialWarningMessage(
|
|
180
|
+
account.name,
|
|
181
|
+
trialExpiresAt,
|
|
182
|
+
now,
|
|
183
|
+
);
|
|
184
|
+
let configured = 0;
|
|
185
|
+
let sent = 0;
|
|
186
|
+
for (const agent of await this.deps.accounts.listAgents(account.id)) {
|
|
187
|
+
const bundle = await this.deps.agentEnvService.getConfigBundle(agent.id);
|
|
188
|
+
const botToken = bundle?.env.SLACK_BOT_TOKEN;
|
|
189
|
+
const channel = bundle?.env.SLACK_ALERT_CHANNEL;
|
|
190
|
+
if (!botToken || !channel) continue;
|
|
191
|
+
configured++;
|
|
192
|
+
if (await this.send({ botToken, channel, text })) sent++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (configured === 0) {
|
|
196
|
+
this.log(
|
|
197
|
+
`[account-trial-warning-sweeper] skip ${account.id} (${account.name}): no agent with SLACK_BOT_TOKEN + SLACK_ALERT_CHANNEL — will retry next tick`,
|
|
198
|
+
);
|
|
199
|
+
result.skipped++;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (sent === 0) {
|
|
203
|
+
console.error(
|
|
204
|
+
`[account-trial-warning-sweeper] every warning post failed for account ${account.id} (${account.name}) — will retry next tick`,
|
|
205
|
+
);
|
|
206
|
+
result.failed++;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await this.deps.accounts.markTrialWarned(account.id, now);
|
|
211
|
+
result.warned++;
|
|
212
|
+
this.log(
|
|
213
|
+
`[account-trial-warning-sweeper] warned ${account.id} (${account.name}) via ${sent} agent(s): trial expires ${trialExpiresAt.toISOString()}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|