@app-vitals/agent-admin 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +43 -0
- package/README.md +72 -2
- package/e2e/agents-page.e2e.ts +338 -0
- package/e2e/chat-degraded.e2e.ts +153 -0
- package/e2e/chat-page.e2e.ts +169 -0
- package/e2e/chat-progress.e2e.ts +206 -0
- package/e2e/chat-thread-page.e2e.ts +444 -0
- package/e2e/login-page.e2e.ts +148 -0
- package/e2e/test-server.ts +427 -0
- package/openapi.json +6098 -0
- package/openapi.platform.json +5553 -0
- package/package.json +62 -4
- package/playwright.config.ts +25 -0
- package/prisma/client/browser.ts +134 -0
- package/prisma/client/client.ts +158 -0
- package/prisma/client/commonInputTypes.ts +578 -0
- package/prisma/client/enums.ts +15 -0
- package/prisma/client/internal/class.ts +424 -0
- package/prisma/client/internal/prismaNamespace.ts +2898 -0
- package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
- package/prisma/client/models/Account.ts +1774 -0
- package/prisma/client/models/AccountInvite.ts +1386 -0
- package/prisma/client/models/AccountMember.ts +1351 -0
- package/prisma/client/models/Agent.ts +4554 -0
- package/prisma/client/models/AgentAsk.ts +1373 -0
- package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
- package/prisma/client/models/AgentCronJob.ts +2416 -0
- package/prisma/client/models/AgentCronRun.ts +2858 -0
- package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
- package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
- package/prisma/client/models/AgentEnv.ts +1421 -0
- package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
- package/prisma/client/models/AgentMember.ts +1316 -0
- package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
- package/prisma/client/models/AgentPlugin.ts +1421 -0
- package/prisma/client/models/AgentToken.ts +1380 -0
- package/prisma/client/models/AgentTool.ts +1351 -0
- package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
- package/prisma/client/models/ChatThreadWatch.ts +1183 -0
- package/prisma/client/models/PushSubscription.ts +1233 -0
- package/prisma/client/models/SessionAlertState.ts +1187 -0
- package/prisma/client/models/SessionFollow.ts +1187 -0
- package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
- package/prisma/client/models.ts +34 -0
- package/prisma/migrations/20260608000000_init/migration.sql +112 -0
- package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
- package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
- package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
- package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
- package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
- package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
- package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
- package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
- package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
- package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
- package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
- package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
- package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
- package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
- package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
- package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
- package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
- package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
- package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
- package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
- package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
- package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
- package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
- package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
- package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
- package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
- package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
- package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
- package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
- package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
- package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
- package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
- package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
- package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
- package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
- package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
- package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
- package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
- package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
- package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
- package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema/coding.prisma +85 -0
- package/prisma/schema/schema.prisma +506 -0
- package/prisma.config.ts +34 -0
- package/pwa-assets/icons/apple-touch-icon.png +0 -0
- package/pwa-assets/icons/favicon-32.png +0 -0
- package/pwa-assets/icons/icon-192.png +0 -0
- package/pwa-assets/icons/icon-512.png +0 -0
- package/pwa-assets/icons/icon-maskable-192.png +0 -0
- package/pwa-assets/icons/icon-maskable-512.png +0 -0
- package/src/account-agent-create.ts +42 -0
- package/src/account-agent-create.unit.test.ts +28 -0
- package/src/account-created-notifier.ts +71 -0
- package/src/account-created-notifier.unit.test.ts +70 -0
- package/src/account-email.ts +9 -0
- package/src/account-email.unit.test.ts +21 -0
- package/src/account-invites.ts +70 -0
- package/src/account-lifecycle.integration.test.ts +329 -0
- package/src/account-lifecycle.ts +206 -0
- package/src/account-lifecycle.unit.test.ts +108 -0
- package/src/account-members.ts +128 -0
- package/src/account-onboarding.integration.test.ts +101 -0
- package/src/account-onboarding.ts +95 -0
- package/src/account-trial-expiry-sweeper.ts +85 -0
- package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
- package/src/account-trial-warning-sweeper.ts +216 -0
- package/src/accounts-api.integration.test.ts +196 -0
- package/src/accounts-api.smoke.test.ts +292 -0
- package/src/accounts-api.ts +289 -0
- package/src/accounts-api.unit.test.ts +93 -0
- package/src/accounts.integration.test.ts +178 -0
- package/src/accounts.ts +208 -0
- package/src/admin-boot-config.ts +574 -0
- package/src/admin-boot-config.unit.test.ts +28 -0
- package/src/admin-boot.boot-order.unit.test.ts +91 -0
- package/src/admin-boot.ts +690 -0
- package/src/admin-dev-login.smoke.test.ts +317 -0
- package/src/admin-extension.ts +432 -0
- package/src/admin-extension.unit.test.ts +532 -0
- package/src/admin-local-agent.smoke.test.ts +1643 -0
- package/src/admin-spec.smoke.test.ts +96 -0
- package/src/admin-spec.ts +186 -0
- package/src/admin-ui-account-nav.ts +20 -0
- package/src/admin-ui-account-pages.ts +131 -0
- package/src/admin-ui-account-pages.unit.test.ts +112 -0
- package/src/admin-ui-account.smoke.test.ts +286 -0
- package/src/admin-ui-account.ts +185 -0
- package/src/admin-ui-accounts-pages.ts +202 -0
- package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
- package/src/admin-ui-accounts.smoke.test.ts +434 -0
- package/src/admin-ui-accounts.ts +266 -0
- package/src/admin-ui-layout.ts +60 -0
- package/src/admin-ui-layout.unit.test.ts +140 -0
- package/src/admin-ui-pages.ts +3489 -0
- package/src/admin-ui-pages.unit.test.ts +4355 -0
- package/src/admin-ui-push-test.smoke.test.ts +125 -0
- package/src/admin-ui-push-test.ts +102 -0
- package/src/admin-ui-styles.ts +656 -0
- package/src/admin-ui-styles.unit.test.ts +103 -0
- package/src/admin-ui.fixtures.ts +457 -0
- package/src/admin-ui.smoke.test.ts +7227 -0
- package/src/admin-ui.ts +3824 -0
- package/src/agent-account-assignment.ts +49 -0
- package/src/agent-account-assignment.unit.test.ts +99 -0
- package/src/agent-anthropic-identity.integration.test.ts +99 -0
- package/src/agent-anthropic-identity.ts +78 -0
- package/src/agent-anthropic-identity.unit.test.ts +77 -0
- package/src/agent-ask-config.ts +59 -0
- package/src/agent-ask-config.unit.test.ts +70 -0
- package/src/agent-ask.integration.test.ts +175 -0
- package/src/agent-ask.smoke.test.ts +457 -0
- package/src/agent-ask.ts +378 -0
- package/src/agent-chat-tokens.integration.test.ts +420 -0
- package/src/agent-chat-tokens.smoke.test.ts +551 -0
- package/src/agent-chat-tokens.ts +259 -0
- package/src/agent-cron-jobs.integration.test.ts +582 -0
- package/src/agent-cron-jobs.ts +615 -0
- package/src/agent-cron-jobs.unit.test.ts +420 -0
- package/src/agent-cron-run-stats.integration.test.ts +1551 -0
- package/src/agent-cron-run-stats.smoke.test.ts +953 -0
- package/src/agent-cron-run-stats.ts +752 -0
- package/src/agent-cron-runs.integration.test.ts +1491 -0
- package/src/agent-cron-runs.ts +489 -0
- package/src/agent-deletion-checklist.ts +128 -0
- package/src/agent-deletion-checklist.unit.test.ts +201 -0
- package/src/agent-deletion-per-agent.integration.test.ts +323 -0
- package/src/agent-deletion.integration.test.ts +323 -0
- package/src/agent-deletion.ts +329 -0
- package/src/agent-deletion.unit.test.ts +412 -0
- package/src/agent-envs.integration.test.ts +323 -0
- package/src/agent-envs.ts +244 -0
- package/src/agent-github-installations.integration.test.ts +115 -0
- package/src/agent-github-installations.ts +52 -0
- package/src/agent-manifest.ts +685 -0
- package/src/agent-manifest.unit.test.ts +870 -0
- package/src/agent-members.ts +69 -0
- package/src/agent-members.unit.test.ts +250 -0
- package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
- package/src/agent-plugins.integration.test.ts +157 -0
- package/src/agent-plugins.ts +95 -0
- package/src/agent-plugins.unit.test.ts +43 -0
- package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
- package/src/agent-provisioner.integration.test.ts +1245 -0
- package/src/agent-provisioner.ts +1650 -0
- package/src/agent-provisioner.unit.test.ts +1852 -0
- package/src/agent-tokens.integration.test.ts +150 -0
- package/src/agent-tokens.ts +112 -0
- package/src/agent-tools.integration.test.ts +129 -0
- package/src/agent-tools.ts +111 -0
- package/src/agents-api.integration.test.ts +705 -0
- package/src/agents-api.sentry.smoke.test.ts +478 -0
- package/src/agents-api.smoke.test.ts +5986 -0
- package/src/agents-api.ts +2826 -0
- package/src/agents.integration.test.ts +512 -0
- package/src/agents.ts +1097 -0
- package/src/agents.unit.test.ts +1736 -0
- package/src/anthropic-credential-guard.ts +44 -0
- package/src/anthropic-credential-guard.unit.test.ts +88 -0
- package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
- package/src/anthropic-federated-token-provider.ts +249 -0
- package/src/anthropic-identity-client.fixtures.ts +237 -0
- package/src/anthropic-identity-client.integration.test.ts +342 -0
- package/src/anthropic-identity-client.ts +473 -0
- package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
- package/src/anthropic-identity-name.integration.test.ts +83 -0
- package/src/anthropic-identity-name.ts +163 -0
- package/src/anthropic-identity-name.unit.test.ts +345 -0
- package/src/anthropic-openapi-contract.fixtures.ts +192 -0
- package/src/api-auth.ts +383 -0
- package/src/api-auth.unit.test.ts +837 -0
- package/src/api.smoke.test.ts +1225 -0
- package/src/api.ts +376 -0
- package/src/attachment-validation.ts +124 -0
- package/src/attachment-validation.unit.test.ts +218 -0
- package/src/caller-scope.ts +131 -0
- package/src/caller-scope.unit.test.ts +116 -0
- package/src/chat-credential-config.ts +22 -0
- package/src/chat-credentials-logging.unit.test.ts +92 -0
- package/src/chat-markers.ts +114 -0
- package/src/chat-markers.unit.test.ts +116 -0
- package/src/chat-service-provisioning-client.integration.test.ts +412 -0
- package/src/chat-service-provisioning-client.ts +221 -0
- package/src/chat.smoke.test.ts +1875 -0
- package/src/clock.ts +47 -0
- package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
- package/src/coding-task-store-provisioning-client.ts +156 -0
- package/src/crypto.ts +83 -0
- package/src/dev-auth-guard.ts +29 -0
- package/src/dev-auth-guard.unit.test.ts +51 -0
- package/src/errors.ts +77 -0
- package/src/errors.unit.test.ts +52 -0
- package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
- package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
- package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
- package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
- package/src/fixtures/github-app-manifest-cassette.json +44 -0
- package/src/fixtures/google-auth-cassette.json +36 -0
- package/src/fixtures/http-chat-client-cassette.json +226 -0
- package/src/fixtures/k8s-cassette.json +470 -0
- package/src/fixtures/okta-auth-cassette.json +36 -0
- package/src/fixtures/slack-provision-cassette.json +12 -0
- package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
- package/src/fixtures/task-store-provisioning-cassette.json +85 -0
- package/src/github-app-provisioning-client.integration.test.ts +149 -0
- package/src/github-app-provisioning-client.ts +161 -0
- package/src/github-app-provisioning-client.unit.test.ts +114 -0
- package/src/github-provisioning-service.ts +569 -0
- package/src/github-provisioning-service.unit.test.ts +123 -0
- package/src/google-auth-client.integration.test.ts +195 -0
- package/src/google-auth-client.ts +121 -0
- package/src/http-chat-client.integration.test.ts +554 -0
- package/src/http-chat-client.ts +533 -0
- package/src/http-chat-client.unit.test.ts +119 -0
- package/src/import-boundary.unit.test.ts +240 -0
- package/src/kubernetes-client.integration.test.ts +697 -0
- package/src/kubernetes-client.ts +1027 -0
- package/src/kubernetes-client.unit.test.ts +726 -0
- package/src/main.smoke.test.ts +91 -0
- package/src/main.ts +21 -0
- package/src/main.unit.test.ts +801 -0
- package/src/okta-auth-client.integration.test.ts +263 -0
- package/src/okta-auth-client.ts +152 -0
- package/src/openapi-schemas.ts +1310 -0
- package/src/openapi-schemas.unit.test.ts +348 -0
- package/src/per-agent-identity.fixtures.ts +305 -0
- package/src/platform-doc-route.ts +40 -0
- package/src/prisma-client.integration.test.ts +57 -0
- package/src/prisma-client.ts +187 -0
- package/src/prisma-client.unit.test.ts +178 -0
- package/src/prisma-tx.ts +23 -0
- package/src/push-content.ts +177 -0
- package/src/push-content.unit.test.ts +213 -0
- package/src/push-crypto.ts +135 -0
- package/src/push-crypto.unit.test.ts +66 -0
- package/src/push-sender.ts +169 -0
- package/src/push-sender.unit.test.ts +107 -0
- package/src/push-service.ts +332 -0
- package/src/push-service.unit.test.ts +679 -0
- package/src/push-toggle.ts +135 -0
- package/src/push-toggle.unit.test.ts +78 -0
- package/src/pwa-https-gating.smoke.test.ts +229 -0
- package/src/pwa-icons.unit.test.ts +41 -0
- package/src/pwa-routes.smoke.test.ts +276 -0
- package/src/pwa.ts +380 -0
- package/src/pwa.unit.test.ts +321 -0
- package/src/schema.unit.test.ts +122 -0
- package/src/self-serve-config.ts +52 -0
- package/src/self-serve-config.unit.test.ts +79 -0
- package/src/server.smoke.test.ts +273 -0
- package/src/server.ts +50 -0
- package/src/service-integration.ts +67 -0
- package/src/service-integration.unit.test.ts +129 -0
- package/src/slack-provisioning-client.integration.test.ts +308 -0
- package/src/slack-provisioning-client.ts +411 -0
- package/src/slack-provisioning-client.unit.test.ts +80 -0
- package/src/slack-provisioning-service.integration.test.ts +637 -0
- package/src/slack-provisioning-service.ts +451 -0
- package/src/slack-provisioning.integration.test.ts +395 -0
- package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
- package/src/specialty-manifest-loader.ts +255 -0
- package/src/specialty-manifest-loader.unit.test.ts +301 -0
- package/src/specialty-registry.schema.unit.test.ts +35 -0
- package/src/specialty-registry.ts +236 -0
- package/src/specialty-registry.unit.test.ts +272 -0
- package/src/startup-policy-reconcile.ts +77 -0
- package/src/startup-policy-reconcile.unit.test.ts +251 -0
- package/src/theme-tokens.json +12 -0
- package/src/token-crypto.ts +60 -0
- package/src/token-crypto.unit.test.ts +182 -0
- package/src/trial-expiry-sweeper.integration.test.ts +318 -0
- package/src/trial-expiry-sweeper.ts +132 -0
- package/src/trial-expiry-sweeper.unit.test.ts +391 -0
- package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
- package/src/trial-expiry-warning-sweeper.ts +447 -0
- package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
- package/src/voice-contract.unit.test.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for agent-cron-jobs.ts:
|
|
3
|
+
* - the pure cron-expression validator (isValidCron)
|
|
4
|
+
* - reconcileSystemCrons()'s create/update/orphan/parent-link logic, driven
|
|
5
|
+
* by an injected SpecialtyManifestResolver double and an in-memory prisma
|
|
6
|
+
* test double (no real DB, no filesystem).
|
|
7
|
+
*
|
|
8
|
+
* Pure logic against injected doubles — see docs/testing.md for the unit-layer
|
|
9
|
+
* contract.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
import type { AgentCronJob, PrismaClient } from "../prisma/client/client.ts";
|
|
13
|
+
import { AgentCronJobService, isValidCron } from "./agent-cron-jobs.ts";
|
|
14
|
+
import {
|
|
15
|
+
type SpecialtyManifestResolver,
|
|
16
|
+
resolveDefaultSpecialtyName,
|
|
17
|
+
} from "./specialty-manifest-loader.ts";
|
|
18
|
+
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
19
|
+
|
|
20
|
+
describe("isValidCron", () => {
|
|
21
|
+
test("accepts standard 5-field schedules", () => {
|
|
22
|
+
expect(isValidCron("0 9 * * *")).toBe(true);
|
|
23
|
+
expect(isValidCron("*/5 * * * *")).toBe(true);
|
|
24
|
+
expect(isValidCron("0 4 * * 1")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// The staggered pipeline schedules use comma-separated minute lists like
|
|
28
|
+
// "0,30" to keep a 30-minute cadence while firing at a distinct offset from
|
|
29
|
+
// sibling crons. Confirm the validator accepts them.
|
|
30
|
+
test("accepts comma-separated minute lists", () => {
|
|
31
|
+
expect(isValidCron("0,30 * * * *")).toBe(true);
|
|
32
|
+
expect(isValidCron("5,35 * * * *")).toBe(true);
|
|
33
|
+
expect(isValidCron("10,40 * * * *")).toBe(true);
|
|
34
|
+
expect(isValidCron("15,45 * * * *")).toBe(true);
|
|
35
|
+
expect(isValidCron("20,50 * * * *")).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("accepts comma lists in other fields", () => {
|
|
39
|
+
expect(isValidCron("0 9 * * 1,3,5")).toBe(true);
|
|
40
|
+
expect(isValidCron("0 9,17 * * *")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejects schedules without exactly 5 fields", () => {
|
|
44
|
+
expect(isValidCron("0 9 * *")).toBe(false);
|
|
45
|
+
expect(isValidCron("0 9 * * * *")).toBe(false);
|
|
46
|
+
expect(isValidCron("")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rejects non-cron garbage", () => {
|
|
50
|
+
expect(isValidCron("not-a-cron")).toBe(false);
|
|
51
|
+
expect(isValidCron("bad")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ─── reconcileSystemCrons() with injected registry + fake prisma ────────────
|
|
56
|
+
|
|
57
|
+
type ManifestCron = SpecialtyManifest["crons"][number];
|
|
58
|
+
|
|
59
|
+
/** Build a minimal valid-enough manifest carrying just the crons we care about. */
|
|
60
|
+
function manifestWithCrons(crons: ManifestCron[]): SpecialtyManifest {
|
|
61
|
+
return { crons } as unknown as SpecialtyManifest;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A registry double that always returns a fixed manifest and records lookups. */
|
|
65
|
+
function fakeRegistry(
|
|
66
|
+
byType: Record<string, ManifestCron[]>,
|
|
67
|
+
fallbackType = "coding",
|
|
68
|
+
): SpecialtyManifestResolver & { lookups: string[] } {
|
|
69
|
+
const lookups: string[] = [];
|
|
70
|
+
return {
|
|
71
|
+
lookups,
|
|
72
|
+
getManifest(typeName: string): SpecialtyManifest {
|
|
73
|
+
lookups.push(typeName);
|
|
74
|
+
const crons = byType[typeName] ?? byType[fallbackType];
|
|
75
|
+
if (!crons) throw new Error(`no manifest for ${typeName}`);
|
|
76
|
+
return manifestWithCrons(crons);
|
|
77
|
+
},
|
|
78
|
+
tryGetManifest(typeName: string): SpecialtyManifest | undefined {
|
|
79
|
+
lookups.push(typeName);
|
|
80
|
+
const crons = byType[typeName];
|
|
81
|
+
return crons ? manifestWithCrons(crons) : undefined;
|
|
82
|
+
},
|
|
83
|
+
listTypes() {
|
|
84
|
+
return Object.keys(byType).map((name) => ({ name, displayName: name }));
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type FakeCronRow = AgentCronJob;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* In-memory prisma double covering only what reconcileSystemCrons() touches:
|
|
93
|
+
* agent.findUnique (typeName resolution),
|
|
94
|
+
* agentCronJob.findMany, and a $transaction exposing
|
|
95
|
+
* agentCronJob.{create,update,delete}.
|
|
96
|
+
*/
|
|
97
|
+
function makeFakePrisma(opts: {
|
|
98
|
+
typeName: string;
|
|
99
|
+
existing?: Partial<FakeCronRow>[];
|
|
100
|
+
}) {
|
|
101
|
+
let nextId = 1;
|
|
102
|
+
const rows = new Map<string, FakeCronRow>();
|
|
103
|
+
for (const seed of opts.existing ?? []) {
|
|
104
|
+
const id = seed.id ?? `existing-${nextId++}`;
|
|
105
|
+
rows.set(id, {
|
|
106
|
+
id,
|
|
107
|
+
agentId: "agent-1",
|
|
108
|
+
name: null,
|
|
109
|
+
schedule: "* * * * *",
|
|
110
|
+
prompt: "",
|
|
111
|
+
channel: null,
|
|
112
|
+
user: null,
|
|
113
|
+
silent: false,
|
|
114
|
+
enabled: true,
|
|
115
|
+
preCheck: null,
|
|
116
|
+
system: true,
|
|
117
|
+
parentCronId: null,
|
|
118
|
+
lockdownDisabledAt: null,
|
|
119
|
+
createdAt: new Date("2024-01-01"),
|
|
120
|
+
updatedAt: new Date("2024-01-01"),
|
|
121
|
+
...seed,
|
|
122
|
+
} as FakeCronRow);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const agentCronJob = {
|
|
126
|
+
async findMany({
|
|
127
|
+
where,
|
|
128
|
+
}: {
|
|
129
|
+
where?: { agentId?: string; system?: boolean };
|
|
130
|
+
} = {}): Promise<FakeCronRow[]> {
|
|
131
|
+
return Array.from(rows.values()).filter((r) => {
|
|
132
|
+
if (where?.agentId !== undefined && r.agentId !== where.agentId)
|
|
133
|
+
return false;
|
|
134
|
+
if (where?.system !== undefined && r.system !== where.system)
|
|
135
|
+
return false;
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
async create({
|
|
140
|
+
data,
|
|
141
|
+
}: {
|
|
142
|
+
data: Partial<FakeCronRow>;
|
|
143
|
+
}): Promise<FakeCronRow> {
|
|
144
|
+
const id = `created-${nextId++}`;
|
|
145
|
+
const row = {
|
|
146
|
+
id,
|
|
147
|
+
agentId: "agent-1",
|
|
148
|
+
name: null,
|
|
149
|
+
schedule: "* * * * *",
|
|
150
|
+
prompt: "",
|
|
151
|
+
channel: null,
|
|
152
|
+
user: null,
|
|
153
|
+
silent: false,
|
|
154
|
+
enabled: true,
|
|
155
|
+
preCheck: null,
|
|
156
|
+
system: true,
|
|
157
|
+
parentCronId: null,
|
|
158
|
+
lockdownDisabledAt: null,
|
|
159
|
+
createdAt: new Date("2024-01-01"),
|
|
160
|
+
updatedAt: new Date("2024-01-01"),
|
|
161
|
+
...data,
|
|
162
|
+
} as FakeCronRow;
|
|
163
|
+
rows.set(id, row);
|
|
164
|
+
return row;
|
|
165
|
+
},
|
|
166
|
+
async update({
|
|
167
|
+
where,
|
|
168
|
+
data,
|
|
169
|
+
}: {
|
|
170
|
+
where: { id: string };
|
|
171
|
+
data: Partial<FakeCronRow>;
|
|
172
|
+
}): Promise<FakeCronRow> {
|
|
173
|
+
const row = rows.get(where.id);
|
|
174
|
+
if (!row) throw new Error(`no row ${where.id}`);
|
|
175
|
+
const next = { ...row, ...data } as FakeCronRow;
|
|
176
|
+
rows.set(where.id, next);
|
|
177
|
+
return next;
|
|
178
|
+
},
|
|
179
|
+
async delete({ where }: { where: { id: string } }): Promise<FakeCronRow> {
|
|
180
|
+
const row = rows.get(where.id);
|
|
181
|
+
if (!row) throw new Error(`no row ${where.id}`);
|
|
182
|
+
rows.delete(where.id);
|
|
183
|
+
return row;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const agent = {
|
|
188
|
+
async findUnique({
|
|
189
|
+
where,
|
|
190
|
+
}: {
|
|
191
|
+
where: { id: string };
|
|
192
|
+
}): Promise<{ typeName: string } | null> {
|
|
193
|
+
return where.id === "agent-1" ? { typeName: opts.typeName } : null;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const prisma = {
|
|
198
|
+
agent,
|
|
199
|
+
agentCronJob,
|
|
200
|
+
async $transaction<T>(
|
|
201
|
+
fn: (tx: { agentCronJob: typeof agentCronJob }) => Promise<T>,
|
|
202
|
+
): Promise<T> {
|
|
203
|
+
return fn({ agentCronJob });
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return { prisma, rows };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const loopCron: ManifestCron = {
|
|
211
|
+
name: "shipwright-loop",
|
|
212
|
+
schedule: "* * * * *",
|
|
213
|
+
prompt: "internal",
|
|
214
|
+
silent: true,
|
|
215
|
+
enabled: false,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const childCron: ManifestCron = {
|
|
219
|
+
name: "shipwright-dev-task",
|
|
220
|
+
schedule: "* * * * *",
|
|
221
|
+
prompt: "/shipwright:dev-task",
|
|
222
|
+
silent: true,
|
|
223
|
+
enabled: true,
|
|
224
|
+
parentCron: "shipwright-loop",
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
describe("reconcileSystemCrons (unit, injected registry)", () => {
|
|
228
|
+
test("resolves the agent's typeName via the injected registry", async () => {
|
|
229
|
+
const registry = fakeRegistry({ coding: [loopCron] });
|
|
230
|
+
const { prisma } = makeFakePrisma({ typeName: "coding" });
|
|
231
|
+
const service = new AgentCronJobService(
|
|
232
|
+
prisma as unknown as PrismaClient,
|
|
233
|
+
undefined,
|
|
234
|
+
registry,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await service.reconcileSystemCrons("agent-1");
|
|
238
|
+
|
|
239
|
+
expect(registry.lookups).toEqual(["coding"]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("creates one row per manifest cron on a fresh agent", async () => {
|
|
243
|
+
const registry = fakeRegistry({ coding: [loopCron, childCron] });
|
|
244
|
+
const { prisma, rows } = makeFakePrisma({ typeName: "coding" });
|
|
245
|
+
const service = new AgentCronJobService(
|
|
246
|
+
prisma as unknown as PrismaClient,
|
|
247
|
+
undefined,
|
|
248
|
+
registry,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const result = await service.reconcileSystemCrons("agent-1");
|
|
252
|
+
|
|
253
|
+
expect(result.created).toBe(2);
|
|
254
|
+
expect(result.updated).toBe(0);
|
|
255
|
+
expect(result.deleted).toBe(0);
|
|
256
|
+
const names = Array.from(rows.values())
|
|
257
|
+
.map((r) => r.name)
|
|
258
|
+
.sort();
|
|
259
|
+
expect(names).toEqual(["shipwright-dev-task", "shipwright-loop"]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("links a child cron to its parent on first reconcile", async () => {
|
|
263
|
+
const registry = fakeRegistry({ coding: [childCron, loopCron] });
|
|
264
|
+
const { prisma, rows } = makeFakePrisma({ typeName: "coding" });
|
|
265
|
+
const service = new AgentCronJobService(
|
|
266
|
+
prisma as unknown as PrismaClient,
|
|
267
|
+
undefined,
|
|
268
|
+
registry,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
await service.reconcileSystemCrons("agent-1");
|
|
272
|
+
|
|
273
|
+
const all = Array.from(rows.values());
|
|
274
|
+
const loop = all.find((r) => r.name === "shipwright-loop");
|
|
275
|
+
const child = all.find((r) => r.name === "shipwright-dev-task");
|
|
276
|
+
expect(child?.parentCronId).toBe(loop?.id as string);
|
|
277
|
+
expect(loop?.parentCronId).toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("updates a matching existing row in place, preserving its id", async () => {
|
|
281
|
+
const registry = fakeRegistry({ coding: [loopCron] });
|
|
282
|
+
const { prisma, rows } = makeFakePrisma({
|
|
283
|
+
typeName: "coding",
|
|
284
|
+
existing: [{ id: "stable-id", name: "shipwright-loop", enabled: false }],
|
|
285
|
+
});
|
|
286
|
+
const service = new AgentCronJobService(
|
|
287
|
+
prisma as unknown as PrismaClient,
|
|
288
|
+
undefined,
|
|
289
|
+
registry,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const result = await service.reconcileSystemCrons("agent-1");
|
|
293
|
+
|
|
294
|
+
expect(result.updated).toBe(1);
|
|
295
|
+
expect(result.created).toBe(0);
|
|
296
|
+
expect(rows.has("stable-id")).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("orphan pass deletes a system cron absent from the manifest", async () => {
|
|
300
|
+
const registry = fakeRegistry({ coding: [loopCron] });
|
|
301
|
+
const { prisma, rows } = makeFakePrisma({
|
|
302
|
+
typeName: "coding",
|
|
303
|
+
existing: [{ id: "orphan", name: "removed-cron" }],
|
|
304
|
+
});
|
|
305
|
+
const service = new AgentCronJobService(
|
|
306
|
+
prisma as unknown as PrismaClient,
|
|
307
|
+
undefined,
|
|
308
|
+
registry,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const result = await service.reconcileSystemCrons("agent-1");
|
|
312
|
+
|
|
313
|
+
expect(result.deleted).toBe(1);
|
|
314
|
+
expect(rows.has("orphan")).toBe(false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("clears a stale parentCronId when the entry no longer declares a resolvable parent", async () => {
|
|
318
|
+
const registry = fakeRegistry({ coding: [loopCron] });
|
|
319
|
+
const { prisma, rows } = makeFakePrisma({
|
|
320
|
+
typeName: "coding",
|
|
321
|
+
existing: [
|
|
322
|
+
{
|
|
323
|
+
id: "loop-id",
|
|
324
|
+
name: "shipwright-loop",
|
|
325
|
+
parentCronId: "some-old-parent",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
const service = new AgentCronJobService(
|
|
330
|
+
prisma as unknown as PrismaClient,
|
|
331
|
+
undefined,
|
|
332
|
+
registry,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
await service.reconcileSystemCrons("agent-1");
|
|
336
|
+
|
|
337
|
+
expect(rows.get("loop-id")?.parentCronId).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("an unknown non-coding typeName logs an error and changes nothing", async () => {
|
|
341
|
+
const registry = fakeRegistry({ coding: [loopCron] });
|
|
342
|
+
const { prisma, rows } = makeFakePrisma({
|
|
343
|
+
typeName: "specialist",
|
|
344
|
+
existing: [
|
|
345
|
+
{ id: "sys-1", name: "old-system", system: true },
|
|
346
|
+
{ id: "user-1", name: "mine", system: false },
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
const service = new AgentCronJobService(
|
|
350
|
+
prisma as unknown as PrismaClient,
|
|
351
|
+
undefined,
|
|
352
|
+
registry,
|
|
353
|
+
);
|
|
354
|
+
const errors: unknown[][] = [];
|
|
355
|
+
const original = console.error;
|
|
356
|
+
console.error = (...args: unknown[]) => {
|
|
357
|
+
errors.push(args);
|
|
358
|
+
};
|
|
359
|
+
let result: Awaited<ReturnType<typeof service.reconcileSystemCrons>>;
|
|
360
|
+
try {
|
|
361
|
+
result = await service.reconcileSystemCrons("agent-1");
|
|
362
|
+
} finally {
|
|
363
|
+
console.error = original;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
expect(result).toEqual({ created: 0, updated: 0, deleted: 0 });
|
|
367
|
+
expect(Array.from(rows.keys()).sort()).toEqual(["sys-1", "user-1"]);
|
|
368
|
+
expect(errors).toHaveLength(1);
|
|
369
|
+
expect(String(errors[0]?.[0])).toContain("specialist");
|
|
370
|
+
expect(String(errors[0]?.[0])).toContain("agent-1");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("the default manifest that cannot load still throws", async () => {
|
|
374
|
+
const registry = fakeRegistry({});
|
|
375
|
+
const { prisma } = makeFakePrisma({
|
|
376
|
+
typeName: resolveDefaultSpecialtyName(),
|
|
377
|
+
});
|
|
378
|
+
const service = new AgentCronJobService(
|
|
379
|
+
prisma as unknown as PrismaClient,
|
|
380
|
+
undefined,
|
|
381
|
+
registry,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await expect(service.reconcileSystemCrons("agent-1")).rejects.toThrow(
|
|
385
|
+
resolveDefaultSpecialtyName(),
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("the default-manifest failure names the env vars that configure it", async () => {
|
|
390
|
+
const registry = fakeRegistry({});
|
|
391
|
+
const { prisma } = makeFakePrisma({
|
|
392
|
+
typeName: resolveDefaultSpecialtyName(),
|
|
393
|
+
});
|
|
394
|
+
const service = new AgentCronJobService(
|
|
395
|
+
prisma as unknown as PrismaClient,
|
|
396
|
+
undefined,
|
|
397
|
+
registry,
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
await expect(service.reconcileSystemCrons("agent-1")).rejects.toThrow(
|
|
401
|
+
/SHIPWRIGHT_SPECIALTIES_DIR.*SHIPWRIGHT_DEFAULT_AGENT_TYPE/s,
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("a valid non-coding manifest reconciles normally", async () => {
|
|
406
|
+
const registry = fakeRegistry({ specialist: [loopCron] });
|
|
407
|
+
const { prisma, rows } = makeFakePrisma({ typeName: "specialist" });
|
|
408
|
+
const service = new AgentCronJobService(
|
|
409
|
+
prisma as unknown as PrismaClient,
|
|
410
|
+
undefined,
|
|
411
|
+
registry,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
const result = await service.reconcileSystemCrons("agent-1");
|
|
415
|
+
|
|
416
|
+
expect(result.created).toBe(1);
|
|
417
|
+
expect(registry.lookups).toEqual(["specialist"]);
|
|
418
|
+
expect(Array.from(rows.values())[0]?.name).toBe("shipwright-loop");
|
|
419
|
+
});
|
|
420
|
+
});
|