@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,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/agent-cron-jobs.integration.test.ts
|
|
3
|
+
* Integration tests for AgentCronJobService against a real PostgreSQL DB.
|
|
4
|
+
*
|
|
5
|
+
* The reconcile tests inject a small fixture specialty, so they do not depend
|
|
6
|
+
* on any shipped specialty. The golden row set for the "coding" specialty
|
|
7
|
+
* lives in agent-cron-jobs.coding.integration.test.ts.
|
|
8
|
+
*
|
|
9
|
+
* Requires DATABASE_URL_ADMIN_TEST to be set; skips otherwise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
13
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
14
|
+
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
15
|
+
import { NotFoundError, UnprocessableEntityError } from "./errors.ts";
|
|
16
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
17
|
+
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
18
|
+
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
19
|
+
|
|
20
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
21
|
+
|
|
22
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
23
|
+
|
|
24
|
+
function makePrisma(): PrismaClient {
|
|
25
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
26
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A fixture specialty with a parent cron, two children, and a standalone
|
|
30
|
+
// cron, covering every field reconcile maps into a row.
|
|
31
|
+
const FIXTURE_TYPE = "fixture";
|
|
32
|
+
|
|
33
|
+
type ManifestCron = SpecialtyManifest["crons"][number];
|
|
34
|
+
|
|
35
|
+
const FIXTURE_CRONS: ManifestCron[] = [
|
|
36
|
+
{
|
|
37
|
+
name: "fixture-loop",
|
|
38
|
+
schedule: "*/30 * * * *",
|
|
39
|
+
prompt: "/fixture:loop",
|
|
40
|
+
silent: true,
|
|
41
|
+
enabled: true,
|
|
42
|
+
preCheck: "fixture:check-loop.ts",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "fixture-child-a",
|
|
46
|
+
schedule: "0,30 * * * *",
|
|
47
|
+
prompt: "/fixture:a",
|
|
48
|
+
silent: true,
|
|
49
|
+
enabled: false,
|
|
50
|
+
parentCron: "fixture-loop",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "fixture-child-b",
|
|
54
|
+
schedule: "15,45 * * * *",
|
|
55
|
+
prompt: "/fixture:b",
|
|
56
|
+
silent: true,
|
|
57
|
+
enabled: false,
|
|
58
|
+
parentCron: "fixture-loop",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "fixture-standalone",
|
|
62
|
+
schedule: "0 7 * * 1",
|
|
63
|
+
prompt: "/fixture:standalone\n/fixture:follow-up",
|
|
64
|
+
silent: false,
|
|
65
|
+
enabled: true,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const fixtureManifest = {
|
|
70
|
+
crons: FIXTURE_CRONS,
|
|
71
|
+
} as unknown as SpecialtyManifest;
|
|
72
|
+
|
|
73
|
+
/** Resolves only FIXTURE_TYPE; every other type is unknown. */
|
|
74
|
+
const fixtureResolver: SpecialtyManifestResolver = {
|
|
75
|
+
getManifest(typeName: string): SpecialtyManifest {
|
|
76
|
+
if (typeName !== FIXTURE_TYPE) throw new Error(`no manifest: ${typeName}`);
|
|
77
|
+
return fixtureManifest;
|
|
78
|
+
},
|
|
79
|
+
tryGetManifest(typeName: string): SpecialtyManifest | undefined {
|
|
80
|
+
return typeName === FIXTURE_TYPE ? fixtureManifest : undefined;
|
|
81
|
+
},
|
|
82
|
+
listTypes() {
|
|
83
|
+
return [{ name: FIXTURE_TYPE, displayName: FIXTURE_TYPE }];
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function createAgent(
|
|
88
|
+
prisma: PrismaClient,
|
|
89
|
+
name = "Test Agent",
|
|
90
|
+
typeName = FIXTURE_TYPE,
|
|
91
|
+
): Promise<string> {
|
|
92
|
+
const agent = await prisma.agent.create({ data: { name, typeName } });
|
|
93
|
+
return agent.id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describeOrSkip("AgentCronJobService (integration)", () => {
|
|
97
|
+
let prisma: PrismaClient;
|
|
98
|
+
let service: AgentCronJobService;
|
|
99
|
+
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
prisma = makePrisma();
|
|
102
|
+
await prisma.agentToken.deleteMany();
|
|
103
|
+
await prisma.agentCronJob.deleteMany();
|
|
104
|
+
await prisma.agentTool.deleteMany();
|
|
105
|
+
await prisma.agentEnv.deleteMany();
|
|
106
|
+
await prisma.agent.deleteMany();
|
|
107
|
+
service = new AgentCronJobService(prisma, undefined, fixtureResolver);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
await prisma.$disconnect();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── create ─────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
it("create() creates a cron job with channel target", async () => {
|
|
117
|
+
const agentId = await createAgent(prisma);
|
|
118
|
+
const job = await service.create(agentId, {
|
|
119
|
+
schedule: "0 9 * * *",
|
|
120
|
+
prompt: "Good morning",
|
|
121
|
+
channel: "C123456",
|
|
122
|
+
});
|
|
123
|
+
expect(job.agentId).toBe(agentId);
|
|
124
|
+
expect(job.schedule).toBe("0 9 * * *");
|
|
125
|
+
expect(job.channel).toBe("C123456");
|
|
126
|
+
expect(job.user).toBeNull();
|
|
127
|
+
expect(job.silent).toBe(false);
|
|
128
|
+
expect(job.enabled).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("create() creates a cron job with user target", async () => {
|
|
132
|
+
const agentId = await createAgent(prisma);
|
|
133
|
+
const job = await service.create(agentId, {
|
|
134
|
+
schedule: "*/5 * * * *",
|
|
135
|
+
prompt: "Check status",
|
|
136
|
+
user: "U123456",
|
|
137
|
+
});
|
|
138
|
+
expect(job.user).toBe("U123456");
|
|
139
|
+
expect(job.channel).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("create() creates a silent cron job with no channel/user", async () => {
|
|
143
|
+
const agentId = await createAgent(prisma);
|
|
144
|
+
const job = await service.create(agentId, {
|
|
145
|
+
schedule: "0 * * * *",
|
|
146
|
+
prompt: "Silent task",
|
|
147
|
+
silent: true,
|
|
148
|
+
});
|
|
149
|
+
expect(job.silent).toBe(true);
|
|
150
|
+
expect(job.channel).toBeNull();
|
|
151
|
+
expect(job.user).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("create() throws UnprocessableEntityError for invalid cron expression", async () => {
|
|
155
|
+
const agentId = await createAgent(prisma);
|
|
156
|
+
await expect(
|
|
157
|
+
service.create(agentId, {
|
|
158
|
+
schedule: "not-a-cron",
|
|
159
|
+
prompt: "Bad cron",
|
|
160
|
+
channel: "C123",
|
|
161
|
+
}),
|
|
162
|
+
).rejects.toBeInstanceOf(UnprocessableEntityError);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("create() throws UnprocessableEntityError when both channel and user are set", async () => {
|
|
166
|
+
const agentId = await createAgent(prisma);
|
|
167
|
+
await expect(
|
|
168
|
+
service.create(agentId, {
|
|
169
|
+
schedule: "0 9 * * *",
|
|
170
|
+
prompt: "Ambiguous target",
|
|
171
|
+
channel: "C123456",
|
|
172
|
+
user: "U123456",
|
|
173
|
+
}),
|
|
174
|
+
).rejects.toBeInstanceOf(UnprocessableEntityError);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("create() throws UnprocessableEntityError when neither channel nor user set and not silent", async () => {
|
|
178
|
+
const agentId = await createAgent(prisma);
|
|
179
|
+
await expect(
|
|
180
|
+
service.create(agentId, {
|
|
181
|
+
schedule: "0 9 * * *",
|
|
182
|
+
prompt: "No target",
|
|
183
|
+
}),
|
|
184
|
+
).rejects.toBeInstanceOf(UnprocessableEntityError);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── list / get ─────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
it("list() returns all cron jobs for an agent", async () => {
|
|
190
|
+
const agentId = await createAgent(prisma);
|
|
191
|
+
await service.create(agentId, {
|
|
192
|
+
schedule: "0 9 * * *",
|
|
193
|
+
prompt: "Job 1",
|
|
194
|
+
channel: "C1",
|
|
195
|
+
});
|
|
196
|
+
await service.create(agentId, {
|
|
197
|
+
schedule: "0 10 * * *",
|
|
198
|
+
prompt: "Job 2",
|
|
199
|
+
channel: "C2",
|
|
200
|
+
});
|
|
201
|
+
const jobs = await service.list(agentId);
|
|
202
|
+
expect(jobs).toHaveLength(2);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("get() returns a specific cron job", async () => {
|
|
206
|
+
const agentId = await createAgent(prisma);
|
|
207
|
+
const created = await service.create(agentId, {
|
|
208
|
+
schedule: "0 9 * * *",
|
|
209
|
+
prompt: "Specific",
|
|
210
|
+
channel: "C1",
|
|
211
|
+
});
|
|
212
|
+
const fetched = await service.get(agentId, created.id);
|
|
213
|
+
expect(fetched.id).toBe(created.id);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("get() throws NotFoundError for unknown cronId", async () => {
|
|
217
|
+
const agentId = await createAgent(prisma);
|
|
218
|
+
await expect(service.get(agentId, "nonexistent")).rejects.toBeInstanceOf(
|
|
219
|
+
NotFoundError,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("get() throws NotFoundError when cronId belongs to a different agent", async () => {
|
|
224
|
+
const agentId1 = await createAgent(prisma, "Agent 1");
|
|
225
|
+
const agentId2 = await createAgent(prisma, "Agent 2");
|
|
226
|
+
const job = await service.create(agentId1, {
|
|
227
|
+
schedule: "0 9 * * *",
|
|
228
|
+
prompt: "Owned by agent1",
|
|
229
|
+
channel: "C1",
|
|
230
|
+
});
|
|
231
|
+
await expect(service.get(agentId2, job.id)).rejects.toBeInstanceOf(
|
|
232
|
+
NotFoundError,
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ─── listEnabled ────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
it("listEnabled() returns only enabled jobs across all agents", async () => {
|
|
239
|
+
const agentId = await createAgent(prisma);
|
|
240
|
+
await service.create(agentId, {
|
|
241
|
+
schedule: "0 9 * * *",
|
|
242
|
+
prompt: "Enabled",
|
|
243
|
+
channel: "C1",
|
|
244
|
+
enabled: true,
|
|
245
|
+
});
|
|
246
|
+
await service.create(agentId, {
|
|
247
|
+
schedule: "0 10 * * *",
|
|
248
|
+
prompt: "Disabled",
|
|
249
|
+
channel: "C2",
|
|
250
|
+
enabled: false,
|
|
251
|
+
});
|
|
252
|
+
const enabled = await service.listEnabled();
|
|
253
|
+
expect(enabled.every((j) => j.enabled)).toBe(true);
|
|
254
|
+
expect(enabled).toHaveLength(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ─── update ─────────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
it("update() changes schedule and prompt", async () => {
|
|
260
|
+
const agentId = await createAgent(prisma);
|
|
261
|
+
const job = await service.create(agentId, {
|
|
262
|
+
schedule: "0 9 * * *",
|
|
263
|
+
prompt: "Original",
|
|
264
|
+
channel: "C1",
|
|
265
|
+
});
|
|
266
|
+
const updated = await service.update(agentId, job.id, {
|
|
267
|
+
schedule: "0 10 * * *",
|
|
268
|
+
prompt: "Updated",
|
|
269
|
+
channel: "C1",
|
|
270
|
+
});
|
|
271
|
+
expect(updated.schedule).toBe("0 10 * * *");
|
|
272
|
+
expect(updated.prompt).toBe("Updated");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("update() throws UnprocessableEntityError when both channel and user are set", async () => {
|
|
276
|
+
const agentId = await createAgent(prisma);
|
|
277
|
+
const job = await service.create(agentId, {
|
|
278
|
+
schedule: "0 9 * * *",
|
|
279
|
+
prompt: "Original",
|
|
280
|
+
channel: "C1",
|
|
281
|
+
});
|
|
282
|
+
await expect(
|
|
283
|
+
service.update(agentId, job.id, {
|
|
284
|
+
schedule: "0 9 * * *",
|
|
285
|
+
prompt: "Updated",
|
|
286
|
+
channel: "C1",
|
|
287
|
+
user: "U1",
|
|
288
|
+
}),
|
|
289
|
+
).rejects.toBeInstanceOf(UnprocessableEntityError);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("update() throws UnprocessableEntityError for invalid cron expression", async () => {
|
|
293
|
+
const agentId = await createAgent(prisma);
|
|
294
|
+
const job = await service.create(agentId, {
|
|
295
|
+
schedule: "0 9 * * *",
|
|
296
|
+
prompt: "Original",
|
|
297
|
+
channel: "C1",
|
|
298
|
+
});
|
|
299
|
+
await expect(
|
|
300
|
+
service.update(agentId, job.id, {
|
|
301
|
+
schedule: "bad",
|
|
302
|
+
prompt: "Updated",
|
|
303
|
+
channel: "C1",
|
|
304
|
+
}),
|
|
305
|
+
).rejects.toBeInstanceOf(UnprocessableEntityError);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ─── delete ─────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
it("delete() removes the cron job", async () => {
|
|
311
|
+
const agentId = await createAgent(prisma);
|
|
312
|
+
const job = await service.create(agentId, {
|
|
313
|
+
schedule: "0 9 * * *",
|
|
314
|
+
prompt: "Delete me",
|
|
315
|
+
channel: "C1",
|
|
316
|
+
});
|
|
317
|
+
await service.delete(agentId, job.id);
|
|
318
|
+
const jobs = await service.list(agentId);
|
|
319
|
+
expect(jobs).toHaveLength(0);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("delete() throws NotFoundError for unknown cronId", async () => {
|
|
323
|
+
const agentId = await createAgent(prisma);
|
|
324
|
+
await expect(service.delete(agentId, "nonexistent")).rejects.toBeInstanceOf(
|
|
325
|
+
NotFoundError,
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ─── setEnabled ─────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
it("setEnabled() toggles a cron job", async () => {
|
|
332
|
+
const agentId = await createAgent(prisma);
|
|
333
|
+
const job = await service.create(agentId, {
|
|
334
|
+
schedule: "0 9 * * *",
|
|
335
|
+
prompt: "Toggle",
|
|
336
|
+
channel: "C1",
|
|
337
|
+
enabled: true,
|
|
338
|
+
});
|
|
339
|
+
const disabled = await service.setEnabled(agentId, job.id, false);
|
|
340
|
+
expect(disabled.enabled).toBe(false);
|
|
341
|
+
const reenabled = await service.setEnabled(agentId, job.id, true);
|
|
342
|
+
expect(reenabled.enabled).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// ─── reconcileSystemCrons ───────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
it("reconcileSystemCrons() creates the specialty's system crons for a new agent", async () => {
|
|
348
|
+
const agentId = await createAgent(prisma);
|
|
349
|
+
const result = await service.reconcileSystemCrons(agentId);
|
|
350
|
+
expect(result).toEqual({
|
|
351
|
+
created: FIXTURE_CRONS.length,
|
|
352
|
+
updated: 0,
|
|
353
|
+
deleted: 0,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const systemJobs = (await service.list(agentId)).filter((j) => j.system);
|
|
357
|
+
expect(systemJobs.length).toBe(FIXTURE_CRONS.length);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("reconcileSystemCrons() updates existing system crons in place", async () => {
|
|
361
|
+
const agentId = await createAgent(prisma);
|
|
362
|
+
await service.reconcileSystemCrons(agentId);
|
|
363
|
+
const result = await service.reconcileSystemCrons(agentId);
|
|
364
|
+
expect(result).toEqual({
|
|
365
|
+
created: 0,
|
|
366
|
+
updated: FIXTURE_CRONS.length,
|
|
367
|
+
deleted: 0,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("reconcileSystemCrons() preserves cron id and AgentCronRun history across a reconcile", async () => {
|
|
372
|
+
const agentId = await createAgent(prisma);
|
|
373
|
+
await service.reconcileSystemCrons(agentId);
|
|
374
|
+
|
|
375
|
+
const jobs = await service.list(agentId);
|
|
376
|
+
const target = jobs.find((j) => j.system && j.name);
|
|
377
|
+
if (!target) {
|
|
378
|
+
throw new Error("expected at least one system cron to be seeded");
|
|
379
|
+
}
|
|
380
|
+
const originalId = target.id;
|
|
381
|
+
|
|
382
|
+
// Record a run against this cron, as the runtime does after executing it.
|
|
383
|
+
const run = await prisma.agentCronRun.create({
|
|
384
|
+
data: {
|
|
385
|
+
cronId: originalId,
|
|
386
|
+
agentId,
|
|
387
|
+
startedAt: new Date(),
|
|
388
|
+
completedAt: new Date(),
|
|
389
|
+
skipped: false,
|
|
390
|
+
outcome: "success",
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Reconciling again (e.g. on agent restart) must not wipe history: it
|
|
395
|
+
// should update the existing row in place rather than delete+recreate it.
|
|
396
|
+
const result = await service.reconcileSystemCrons(agentId);
|
|
397
|
+
expect(result.updated).toBe(FIXTURE_CRONS.length);
|
|
398
|
+
expect(result.created).toBe(0);
|
|
399
|
+
|
|
400
|
+
const jobsAfter = await service.list(agentId);
|
|
401
|
+
const targetAfter = jobsAfter.find((j) => j.name === target.name);
|
|
402
|
+
expect(targetAfter).toBeDefined();
|
|
403
|
+
expect(targetAfter?.id).toBe(originalId);
|
|
404
|
+
|
|
405
|
+
const survivingRun = await prisma.agentCronRun.findUnique({
|
|
406
|
+
where: { id: run.id },
|
|
407
|
+
});
|
|
408
|
+
expect(survivingRun).not.toBeNull();
|
|
409
|
+
expect(survivingRun?.cronId).toBe(originalId);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("reconcileSystemCrons() links children to their parent on a fresh agent's first reconcile", async () => {
|
|
413
|
+
const agentId = await createAgent(prisma);
|
|
414
|
+
// Fresh agent: no parent row exists yet before this call, so the parent
|
|
415
|
+
// id must be resolved within the same reconcile that creates it.
|
|
416
|
+
await service.reconcileSystemCrons(agentId);
|
|
417
|
+
|
|
418
|
+
const jobs = await service.list(agentId);
|
|
419
|
+
const parent = jobs.find((j) => j.name === "fixture-loop");
|
|
420
|
+
expect(parent).toBeDefined();
|
|
421
|
+
|
|
422
|
+
for (const name of ["fixture-child-a", "fixture-child-b"]) {
|
|
423
|
+
const cron = jobs.find((j) => j.name === name);
|
|
424
|
+
expect(cron).toBeDefined();
|
|
425
|
+
expect(cron?.parentCronId).toBe(parent?.id as string);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Crons that declare no parent must not be linked, the parent included.
|
|
429
|
+
expect(parent?.parentCronId).toBeNull();
|
|
430
|
+
const standalone = jobs.find((j) => j.name === "fixture-standalone");
|
|
431
|
+
expect(standalone?.parentCronId).toBeNull();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("reconcileSystemCrons() self-heals the parentCronId link on a subsequent reconcile", async () => {
|
|
435
|
+
const agentId = await createAgent(prisma);
|
|
436
|
+
await service.reconcileSystemCrons(agentId);
|
|
437
|
+
|
|
438
|
+
// Simulate an agent seeded before parentCron was introduced: clear the
|
|
439
|
+
// parent links on every system row.
|
|
440
|
+
await prisma.agentCronJob.updateMany({
|
|
441
|
+
where: { agentId, system: true },
|
|
442
|
+
data: { parentCronId: null },
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await service.reconcileSystemCrons(agentId);
|
|
446
|
+
|
|
447
|
+
const jobs = await service.list(agentId);
|
|
448
|
+
const parent = jobs.find((j) => j.name === "fixture-loop");
|
|
449
|
+
expect(parent).toBeDefined();
|
|
450
|
+
|
|
451
|
+
for (const name of ["fixture-child-a", "fixture-child-b"]) {
|
|
452
|
+
const cron = jobs.find((j) => j.name === name);
|
|
453
|
+
expect(cron?.parentCronId).toBe(parent?.id as string);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("reconcileSystemCrons() clears a stale parentCronId back to null when the entry no longer declares a resolvable parentCron", async () => {
|
|
458
|
+
const agentId = await createAgent(prisma);
|
|
459
|
+
await service.reconcileSystemCrons(agentId);
|
|
460
|
+
|
|
461
|
+
const jobs = await service.list(agentId);
|
|
462
|
+
const parent = jobs.find((j) => j.name === "fixture-loop");
|
|
463
|
+
const standalone = jobs.find((j) => j.name === "fixture-standalone");
|
|
464
|
+
expect(parent).toBeDefined();
|
|
465
|
+
expect(standalone).toBeDefined();
|
|
466
|
+
|
|
467
|
+
// Simulate a stale link: the manifest never declares a parentCron for
|
|
468
|
+
// "fixture-standalone", but its row has a non-null parentCronId — e.g.
|
|
469
|
+
// left over from a since-removed parentCron declaration.
|
|
470
|
+
await prisma.agentCronJob.update({
|
|
471
|
+
where: { id: standalone?.id as string },
|
|
472
|
+
data: { parentCronId: parent?.id as string },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const result = await service.reconcileSystemCrons(agentId);
|
|
476
|
+
expect(result.created).toBe(0);
|
|
477
|
+
|
|
478
|
+
const jobsAfter = await service.list(agentId);
|
|
479
|
+
const standaloneAfter = jobsAfter.find(
|
|
480
|
+
(j) => j.name === "fixture-standalone",
|
|
481
|
+
);
|
|
482
|
+
expect(standaloneAfter?.parentCronId).toBeNull();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// The expected rows are hardcoded here (NOT derived from the manifest at
|
|
486
|
+
// runtime), so a regression in either the manifest→row mapping or the
|
|
487
|
+
// fixture itself is caught rather than silently accepted.
|
|
488
|
+
it("reconcileSystemCrons() produces the exact row set the manifest declares", async () => {
|
|
489
|
+
const agentId = await createAgent(prisma);
|
|
490
|
+
await service.reconcileSystemCrons(agentId);
|
|
491
|
+
|
|
492
|
+
const systemJobs = (await service.list(agentId)).filter((j) => j.system);
|
|
493
|
+
expect(systemJobs.length).toBe(4);
|
|
494
|
+
|
|
495
|
+
const idByName = new Map<string, string>();
|
|
496
|
+
for (const j of systemJobs) {
|
|
497
|
+
if (j.name) idByName.set(j.name, j.id);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const expected = [
|
|
501
|
+
{
|
|
502
|
+
name: "fixture-loop",
|
|
503
|
+
schedule: "*/30 * * * *",
|
|
504
|
+
prompt: "/fixture:loop",
|
|
505
|
+
silent: true,
|
|
506
|
+
preCheck: "fixture:check-loop.ts",
|
|
507
|
+
enabled: true,
|
|
508
|
+
parent: null,
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "fixture-child-a",
|
|
512
|
+
schedule: "0,30 * * * *",
|
|
513
|
+
prompt: "/fixture:a",
|
|
514
|
+
silent: true,
|
|
515
|
+
preCheck: null,
|
|
516
|
+
enabled: false,
|
|
517
|
+
parent: "fixture-loop",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "fixture-child-b",
|
|
521
|
+
schedule: "15,45 * * * *",
|
|
522
|
+
prompt: "/fixture:b",
|
|
523
|
+
silent: true,
|
|
524
|
+
preCheck: null,
|
|
525
|
+
enabled: false,
|
|
526
|
+
parent: "fixture-loop",
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "fixture-standalone",
|
|
530
|
+
schedule: "0 7 * * 1",
|
|
531
|
+
prompt: "/fixture:standalone\n/fixture:follow-up",
|
|
532
|
+
silent: false,
|
|
533
|
+
preCheck: null,
|
|
534
|
+
enabled: true,
|
|
535
|
+
parent: null,
|
|
536
|
+
},
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
for (const want of expected) {
|
|
540
|
+
const row = systemJobs.find((j) => j.name === want.name);
|
|
541
|
+
expect(row, `missing cron ${want.name}`).toBeDefined();
|
|
542
|
+
if (!row) continue;
|
|
543
|
+
|
|
544
|
+
expect(row.schedule).toBe(want.schedule);
|
|
545
|
+
expect(row.prompt).toBe(want.prompt);
|
|
546
|
+
expect(row.silent).toBe(want.silent);
|
|
547
|
+
expect(row.preCheck).toBe(want.preCheck);
|
|
548
|
+
expect(row.enabled).toBe(want.enabled);
|
|
549
|
+
expect(row.system).toBe(true);
|
|
550
|
+
expect(row.channel).toBeNull();
|
|
551
|
+
expect(row.user).toBeNull();
|
|
552
|
+
expect(row.parentCronId).toBe(
|
|
553
|
+
want.parent ? (idByName.get(want.parent) ?? null) : null,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("reconcileSystemCrons() is idempotent — a second reconcile reproduces the same rows with no orphans", async () => {
|
|
559
|
+
const agentId = await createAgent(prisma);
|
|
560
|
+
await service.reconcileSystemCrons(agentId);
|
|
561
|
+
const second = await service.reconcileSystemCrons(agentId);
|
|
562
|
+
|
|
563
|
+
// Second pass updates every row in place; creates and deletes nothing.
|
|
564
|
+
expect(second).toEqual({
|
|
565
|
+
created: 0,
|
|
566
|
+
updated: FIXTURE_CRONS.length,
|
|
567
|
+
deleted: 0,
|
|
568
|
+
});
|
|
569
|
+
const systemJobs = (await service.list(agentId)).filter((j) => j.system);
|
|
570
|
+
expect(systemJobs.length).toBe(FIXTURE_CRONS.length);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("reconcileSystemCrons() for an unknown typeName changes nothing (no fallback)", async () => {
|
|
574
|
+
const agentId = await createAgent(prisma, "Renamed Agent", "renamed-type");
|
|
575
|
+
|
|
576
|
+
// Must not throw, and must not seed another specialty's crons.
|
|
577
|
+
const result = await service.reconcileSystemCrons(agentId);
|
|
578
|
+
|
|
579
|
+
expect(result).toEqual({ created: 0, updated: 0, deleted: 0 });
|
|
580
|
+
expect(await service.list(agentId)).toEqual([]);
|
|
581
|
+
});
|
|
582
|
+
});
|