@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/trial-expiry-sweeper.integration.test.ts
|
|
3
|
+
* Integration tests for TrialExpirySweeper against a real PostgreSQL DB —
|
|
4
|
+
* exercises the real AgentCronJobService.listEnabledWithExpiredTrial() join
|
|
5
|
+
* query and setEnabled() write path together (ATE-3.1).
|
|
6
|
+
*
|
|
7
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise (mirrors
|
|
8
|
+
* agent-cron-jobs.integration.test.ts).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
12
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
13
|
+
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
14
|
+
import { FixedClock } from "./clock.ts";
|
|
15
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
16
|
+
import { TrialExpirySweeper } from "./trial-expiry-sweeper.ts";
|
|
17
|
+
|
|
18
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
19
|
+
|
|
20
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
21
|
+
|
|
22
|
+
function makePrisma(): PrismaClient {
|
|
23
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
24
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const NOW = new Date("2026-06-15T00:00:00.000Z");
|
|
28
|
+
const PAST = new Date("2026-06-01T00:00:00.000Z"); // before NOW — expired
|
|
29
|
+
const FUTURE = new Date("2026-07-01T00:00:00.000Z"); // after NOW — not expired
|
|
30
|
+
|
|
31
|
+
async function createAgent(
|
|
32
|
+
prisma: PrismaClient,
|
|
33
|
+
opts: { name?: string; trialExpiresAt?: Date | null } = {},
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const agent = await prisma.agent.create({
|
|
36
|
+
data: {
|
|
37
|
+
name: opts.name ?? "Test Agent",
|
|
38
|
+
typeName: "coding",
|
|
39
|
+
trialExpiresAt: opts.trialExpiresAt ?? null,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return agent.id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function createCron(
|
|
46
|
+
prisma: PrismaClient,
|
|
47
|
+
agentId: string,
|
|
48
|
+
opts: { enabled?: boolean; name?: string } = {},
|
|
49
|
+
): Promise<string> {
|
|
50
|
+
const cron = await prisma.agentCronJob.create({
|
|
51
|
+
data: {
|
|
52
|
+
agentId,
|
|
53
|
+
schedule: "0 * * * *",
|
|
54
|
+
prompt: "test prompt",
|
|
55
|
+
channel: "C123456",
|
|
56
|
+
silent: false,
|
|
57
|
+
enabled: opts.enabled ?? true,
|
|
58
|
+
name: opts.name ?? null,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return cron.id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getCronEnabled(
|
|
65
|
+
prisma: PrismaClient,
|
|
66
|
+
cronId: string,
|
|
67
|
+
): Promise<boolean> {
|
|
68
|
+
const cron = await prisma.agentCronJob.findUniqueOrThrow({
|
|
69
|
+
where: { id: cronId },
|
|
70
|
+
});
|
|
71
|
+
return cron.enabled;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describeOrSkip("TrialExpirySweeper (integration)", () => {
|
|
75
|
+
let prisma: PrismaClient;
|
|
76
|
+
let sweeper: TrialExpirySweeper;
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
prisma = makePrisma();
|
|
80
|
+
await prisma.agentToken.deleteMany();
|
|
81
|
+
await prisma.agentCronJob.deleteMany();
|
|
82
|
+
await prisma.agentTool.deleteMany();
|
|
83
|
+
await prisma.agentEnv.deleteMany();
|
|
84
|
+
await prisma.agent.deleteMany();
|
|
85
|
+
|
|
86
|
+
const agentCronJobService = new AgentCronJobService(prisma);
|
|
87
|
+
sweeper = new TrialExpirySweeper({
|
|
88
|
+
agentCronJobService,
|
|
89
|
+
clock: FixedClock(NOW),
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(async () => {
|
|
94
|
+
await prisma.$disconnect();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── AC1: expired trial → every enabled cron disabled ──────────────────────
|
|
98
|
+
|
|
99
|
+
it("disables every enabled cron for an agent whose trial has expired", async () => {
|
|
100
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
101
|
+
const cronA = await createCron(prisma, agentId, { enabled: true });
|
|
102
|
+
const cronB = await createCron(prisma, agentId, { enabled: true });
|
|
103
|
+
|
|
104
|
+
const result = await sweeper.tick();
|
|
105
|
+
|
|
106
|
+
expect(result.disabled).toBe(2);
|
|
107
|
+
expect(await getCronEnabled(prisma, cronA)).toBe(false);
|
|
108
|
+
expect(await getCronEnabled(prisma, cronB)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("leaves an already-disabled cron for an expired-trial agent untouched (not recounted)", async () => {
|
|
112
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
113
|
+
const enabledCron = await createCron(prisma, agentId, { enabled: true });
|
|
114
|
+
const disabledCron = await createCron(prisma, agentId, {
|
|
115
|
+
enabled: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await sweeper.tick();
|
|
119
|
+
|
|
120
|
+
expect(result.disabled).toBe(1);
|
|
121
|
+
expect(await getCronEnabled(prisma, enabledCron)).toBe(false);
|
|
122
|
+
expect(await getCronEnabled(prisma, disabledCron)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("is idempotent — a second tick after the first finds nothing left to do", async () => {
|
|
126
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
127
|
+
await createCron(prisma, agentId, { enabled: true });
|
|
128
|
+
|
|
129
|
+
const first = await sweeper.tick();
|
|
130
|
+
const second = await sweeper.tick();
|
|
131
|
+
|
|
132
|
+
expect(first.disabled).toBe(1);
|
|
133
|
+
expect(second.disabled).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ─── AC2: not-yet-expired / unset trial → no-op ─────────────────────────────
|
|
137
|
+
|
|
138
|
+
it("does not touch crons for an agent whose trial has not yet expired", async () => {
|
|
139
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: FUTURE });
|
|
140
|
+
const cronId = await createCron(prisma, agentId, { enabled: true });
|
|
141
|
+
|
|
142
|
+
const result = await sweeper.tick();
|
|
143
|
+
|
|
144
|
+
expect(result.disabled).toBe(0);
|
|
145
|
+
expect(await getCronEnabled(prisma, cronId)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("does not touch crons for an agent with trialExpiresAt unset (null)", async () => {
|
|
149
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: null });
|
|
150
|
+
const cronId = await createCron(prisma, agentId, { enabled: true });
|
|
151
|
+
|
|
152
|
+
const result = await sweeper.tick();
|
|
153
|
+
|
|
154
|
+
expect(result.disabled).toBe(0);
|
|
155
|
+
expect(await getCronEnabled(prisma, cronId)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("treats trialExpiresAt exactly equal to now as NOT yet expired", async () => {
|
|
159
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: NOW });
|
|
160
|
+
const cronId = await createCron(prisma, agentId, { enabled: true });
|
|
161
|
+
|
|
162
|
+
const result = await sweeper.tick();
|
|
163
|
+
|
|
164
|
+
expect(result.disabled).toBe(0);
|
|
165
|
+
expect(await getCronEnabled(prisma, cronId)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── Edge cases ──────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
it("no-ops cleanly for an expired-trial agent with zero cron rows", async () => {
|
|
171
|
+
await createAgent(prisma, { trialExpiresAt: PAST });
|
|
172
|
+
|
|
173
|
+
const result = await sweeper.tick();
|
|
174
|
+
|
|
175
|
+
expect(result.disabled).toBe(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("only disables crons for the expired agent, leaving a sibling not-yet-expired agent's crons alone", async () => {
|
|
179
|
+
const expiredAgentId = await createAgent(prisma, {
|
|
180
|
+
name: "Expired Agent",
|
|
181
|
+
trialExpiresAt: PAST,
|
|
182
|
+
});
|
|
183
|
+
const activeAgentId = await createAgent(prisma, {
|
|
184
|
+
name: "Active Agent",
|
|
185
|
+
trialExpiresAt: FUTURE,
|
|
186
|
+
});
|
|
187
|
+
const expiredCron = await createCron(prisma, expiredAgentId, {
|
|
188
|
+
enabled: true,
|
|
189
|
+
});
|
|
190
|
+
const activeCron = await createCron(prisma, activeAgentId, {
|
|
191
|
+
enabled: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await sweeper.tick();
|
|
195
|
+
|
|
196
|
+
expect(result.disabled).toBe(1);
|
|
197
|
+
expect(await getCronEnabled(prisma, expiredCron)).toBe(false);
|
|
198
|
+
expect(await getCronEnabled(prisma, activeCron)).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("a bad row does not abort the rest of the sweep (per-row try/catch)", async () => {
|
|
202
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
203
|
+
const badCronId = await createCron(prisma, agentId, { enabled: true });
|
|
204
|
+
const goodCronId = await createCron(prisma, agentId, { enabled: true });
|
|
205
|
+
|
|
206
|
+
// A fake service whose setEnabled() throws for one specific cron id but
|
|
207
|
+
// succeeds for the other — exercises the per-row try/catch directly
|
|
208
|
+
// against real listing data from the real service.
|
|
209
|
+
const realService = new AgentCronJobService(prisma);
|
|
210
|
+
const flaky = {
|
|
211
|
+
listEnabledWithExpiredTrial: (now: Date) =>
|
|
212
|
+
realService.listEnabledWithExpiredTrial(now),
|
|
213
|
+
setEnabled: async (agId: string, cronId: string, enabled: boolean) => {
|
|
214
|
+
if (cronId === badCronId) {
|
|
215
|
+
throw new Error("boom");
|
|
216
|
+
}
|
|
217
|
+
return realService.setEnabled(agId, cronId, enabled);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
const flakySweeper = new TrialExpirySweeper({
|
|
221
|
+
agentCronJobService: flaky,
|
|
222
|
+
clock: FixedClock(NOW),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await flakySweeper.tick();
|
|
226
|
+
|
|
227
|
+
expect(result.disabled).toBe(1);
|
|
228
|
+
expect(await getCronEnabled(prisma, badCronId)).toBe(true);
|
|
229
|
+
expect(await getCronEnabled(prisma, goodCronId)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─── SSP-8.1: lockdown marker + restore ─────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
it("stamps lockdownDisabledAt on swept crons but not on user-disabled ones", async () => {
|
|
235
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
236
|
+
const swept = await createCron(prisma, agentId, { enabled: true });
|
|
237
|
+
const userDisabled = await createCron(prisma, agentId, { enabled: false });
|
|
238
|
+
|
|
239
|
+
await sweeper.tick();
|
|
240
|
+
|
|
241
|
+
const sweptRow = await prisma.agentCronJob.findUniqueOrThrow({
|
|
242
|
+
where: { id: swept },
|
|
243
|
+
});
|
|
244
|
+
const userRow = await prisma.agentCronJob.findUniqueOrThrow({
|
|
245
|
+
where: { id: userDisabled },
|
|
246
|
+
});
|
|
247
|
+
expect(sweptRow.enabled).toBe(false);
|
|
248
|
+
expect(sweptRow.lockdownDisabledAt).not.toBeNull();
|
|
249
|
+
expect(userRow.lockdownDisabledAt).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("restoreLockdownDisabled re-enables only lockdown-disabled crons and clears the marker", async () => {
|
|
253
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
254
|
+
const swept = await createCron(prisma, agentId, { enabled: true });
|
|
255
|
+
const userDisabled = await createCron(prisma, agentId, { enabled: false });
|
|
256
|
+
await sweeper.tick();
|
|
257
|
+
|
|
258
|
+
const service = new AgentCronJobService(prisma);
|
|
259
|
+
const restored = await service.restoreLockdownDisabled([agentId]);
|
|
260
|
+
|
|
261
|
+
expect(restored).toBe(1);
|
|
262
|
+
const sweptRow = await prisma.agentCronJob.findUniqueOrThrow({
|
|
263
|
+
where: { id: swept },
|
|
264
|
+
});
|
|
265
|
+
expect(sweptRow.enabled).toBe(true);
|
|
266
|
+
expect(sweptRow.lockdownDisabledAt).toBeNull();
|
|
267
|
+
expect(await getCronEnabled(prisma, userDisabled)).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("restoreLockdownDisabled ignores agents not listed and empty input", async () => {
|
|
271
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
272
|
+
const other = await createAgent(prisma, { name: "Other" });
|
|
273
|
+
const cron = await createCron(prisma, agentId, { enabled: true });
|
|
274
|
+
await sweeper.tick();
|
|
275
|
+
|
|
276
|
+
const service = new AgentCronJobService(prisma);
|
|
277
|
+
expect(await service.restoreLockdownDisabled([])).toBe(0);
|
|
278
|
+
expect(await service.restoreLockdownDisabled([other])).toBe(0);
|
|
279
|
+
expect(await getCronEnabled(prisma, cron)).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("a manual setEnabled call clears a stale lockdown marker", async () => {
|
|
283
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
284
|
+
const cron = await createCron(prisma, agentId, { enabled: true });
|
|
285
|
+
await sweeper.tick();
|
|
286
|
+
|
|
287
|
+
const service = new AgentCronJobService(prisma);
|
|
288
|
+
await service.setEnabled(agentId, cron, true);
|
|
289
|
+
const row = await prisma.agentCronJob.findUniqueOrThrow({
|
|
290
|
+
where: { id: cron },
|
|
291
|
+
});
|
|
292
|
+
expect(row.lockdownDisabledAt).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("a content update() with an explicit enabled value clears the lockdown marker", async () => {
|
|
296
|
+
const agentId = await createAgent(prisma, { trialExpiresAt: PAST });
|
|
297
|
+
const cron = await createCron(prisma, agentId, { enabled: true });
|
|
298
|
+
await sweeper.tick();
|
|
299
|
+
|
|
300
|
+
const service = new AgentCronJobService(prisma);
|
|
301
|
+
const before = await prisma.agentCronJob.findUniqueOrThrow({
|
|
302
|
+
where: { id: cron },
|
|
303
|
+
});
|
|
304
|
+
await service.update(agentId, cron, {
|
|
305
|
+
schedule: before.schedule,
|
|
306
|
+
prompt: before.prompt,
|
|
307
|
+
channel: before.channel,
|
|
308
|
+
enabled: false,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const row = await prisma.agentCronJob.findUniqueOrThrow({
|
|
312
|
+
where: { id: cron },
|
|
313
|
+
});
|
|
314
|
+
expect(row.lockdownDisabledAt).toBeNull();
|
|
315
|
+
expect(await service.restoreLockdownDisabled([agentId])).toBe(0);
|
|
316
|
+
expect(await getCronEnabled(prisma, cron)).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/trial-expiry-sweeper.ts
|
|
3
|
+
*
|
|
4
|
+
* TrialExpirySweeper — disables crons for agents whose trial has expired
|
|
5
|
+
* (ATE-3.1). Every tick it finds every enabled AgentCronJob belonging to an
|
|
6
|
+
* agent whose trialExpiresAt has passed and PATCHes each to enabled:false (stamping
|
|
7
|
+
* lockdownDisabledAt so the lockdown can later be undone) via
|
|
8
|
+
* AgentCronJobService.setEnabled() — the same service method the
|
|
9
|
+
* PATCH /agents/:id/crons/:cronId route itself calls (admin/src/agents-api.ts),
|
|
10
|
+
* invoked in-process rather than over HTTP (mirrors session-alert-sweeper.ts
|
|
11
|
+
* calling services directly instead of self-calling its own admin API).
|
|
12
|
+
*
|
|
13
|
+
* Deliberately does NOT deprovision the agent — no deleteAgentFully() call,
|
|
14
|
+
* no touching the K8s workload, tokens, or chat history. A trial that ends
|
|
15
|
+
* should not destroy the workspace, since the customer may convert to paid
|
|
16
|
+
* later. This sweeper only flips AgentCronJob.enabled; agent/src/runtime/slack.ts's
|
|
17
|
+
* isTrialExpired() gate is the other half of the lockdown, blocking inbound
|
|
18
|
+
* Slack messages.
|
|
19
|
+
*
|
|
20
|
+
* Naturally idempotent — once every cron for an expired-trial agent is
|
|
21
|
+
* disabled, listEnabledWithExpiredTrial() finds nothing for that agent on the
|
|
22
|
+
* next tick, so no extra "have I already swept this agent" state is needed
|
|
23
|
+
* (unlike session-alert-sweeper's SessionAlertState dedup). No re-entrancy
|
|
24
|
+
* guard either: overlapping ticks both disabling the same already-disabled
|
|
25
|
+
* row is harmless (setEnabled is idempotent), unlike the session sweeper's
|
|
26
|
+
* push-notification side effects.
|
|
27
|
+
*
|
|
28
|
+
* Structure mirrors session-alert-sweeper.ts: an injected Clock, a per-row
|
|
29
|
+
* try/catch so one bad cron can never abort the rest of the sweep, a counted
|
|
30
|
+
* return value, and registration via setInterval in main.ts (never inside an
|
|
31
|
+
* app factory, which must stay side-effect-free).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { type Clock, SystemClock } from "./clock.ts";
|
|
35
|
+
|
|
36
|
+
/** The narrow slice of AgentCronJobService this sweeper touches. */
|
|
37
|
+
export interface TrialExpiryCronJobServiceLike {
|
|
38
|
+
listEnabledWithExpiredTrial(
|
|
39
|
+
now: Date,
|
|
40
|
+
): Promise<Array<{ id: string; agentId: string }>>;
|
|
41
|
+
setEnabled(
|
|
42
|
+
agentId: string,
|
|
43
|
+
cronId: string,
|
|
44
|
+
enabled: boolean,
|
|
45
|
+
opts?: { lockdown?: boolean },
|
|
46
|
+
): Promise<unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TrialExpirySweeperDeps {
|
|
50
|
+
agentCronJobService: TrialExpiryCronJobServiceLike;
|
|
51
|
+
clock?: Clock;
|
|
52
|
+
/** Line logger for per-row outcomes. Defaults to console.log. */
|
|
53
|
+
log?: (line: string) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Per-tick counter, also the value `tick()` resolves to. */
|
|
57
|
+
export interface TrialExpirySweepResult {
|
|
58
|
+
disabled: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Default cadence of the trial-expiry sweeper. */
|
|
62
|
+
export const DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS = 60_000;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the trial-expiry sweeper's tick interval from the environment.
|
|
66
|
+
* Reads SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS; anything unset,
|
|
67
|
+
* blank, non-numeric, or non-positive falls back to the default rather than
|
|
68
|
+
* producing a setInterval that spins (0/NaN) or never fires.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveTrialExpirySweepIntervalMs(
|
|
71
|
+
env: Record<string, string | undefined>,
|
|
72
|
+
): number {
|
|
73
|
+
const raw = env.SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_SWEEP_INTERVAL_MS?.trim();
|
|
74
|
+
if (!raw) return DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS;
|
|
75
|
+
const parsed = Number(raw);
|
|
76
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
77
|
+
return DEFAULT_TRIAL_EXPIRY_SWEEP_INTERVAL_MS;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class TrialExpirySweeper {
|
|
83
|
+
private readonly clock: Clock;
|
|
84
|
+
private readonly log: (line: string) => void;
|
|
85
|
+
|
|
86
|
+
constructor(private readonly deps: TrialExpirySweeperDeps) {
|
|
87
|
+
this.clock = deps.clock ?? SystemClock();
|
|
88
|
+
this.log = deps.log ?? ((line) => console.log(line));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One sweep: disables every enabled AgentCronJob belonging to an agent
|
|
93
|
+
* whose trial has expired. Never throws — a listing failure short-circuits
|
|
94
|
+
* to an all-zero result, and each row's setEnabled() call is individually
|
|
95
|
+
* try/caught so one bad row can't abort the rest.
|
|
96
|
+
*/
|
|
97
|
+
async tick(): Promise<TrialExpirySweepResult> {
|
|
98
|
+
const now = this.clock.now();
|
|
99
|
+
let rows: Array<{ id: string; agentId: string }>;
|
|
100
|
+
try {
|
|
101
|
+
rows =
|
|
102
|
+
await this.deps.agentCronJobService.listEnabledWithExpiredTrial(now);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error("[trial-expiry-sweeper] listing failed:", err);
|
|
105
|
+
return { disabled: 0 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let disabled = 0;
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
try {
|
|
111
|
+
await this.deps.agentCronJobService.setEnabled(
|
|
112
|
+
row.agentId,
|
|
113
|
+
row.id,
|
|
114
|
+
false,
|
|
115
|
+
{ lockdown: true },
|
|
116
|
+
);
|
|
117
|
+
disabled++;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(
|
|
120
|
+
`[trial-expiry-sweeper] disable cron ${row.id} (agent ${row.agentId}) failed:`,
|
|
121
|
+
err,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (disabled > 0) {
|
|
127
|
+
this.log(`[trial-expiry-sweeper] disabled=${disabled}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { disabled };
|
|
131
|
+
}
|
|
132
|
+
}
|