@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,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/agents.integration.test.ts
|
|
3
|
+
* Integration tests for createAgent() (admin/src/agents.ts) — the atomic
|
|
4
|
+
* agent-creation orchestrator extracted from admin-ui.ts's POST /admin/agents
|
|
5
|
+
* form handler (APA-1.1).
|
|
6
|
+
*
|
|
7
|
+
* Requires DATABASE_URL_ADMIN_TEST; skips otherwise (mirrors
|
|
8
|
+
* agents-api.integration.test.ts's gating pattern).
|
|
9
|
+
*
|
|
10
|
+
* Runs against a REAL Postgres DB — not mocks — specifically to verify the
|
|
11
|
+
* acceptance-criteria claim that can't be proven with injected test doubles:
|
|
12
|
+
* that the DB-writing steps (create, tool/plugin seeding, repos/allowlists/
|
|
13
|
+
* members) run inside a single real Prisma transaction, so a mid-sequence
|
|
14
|
+
* failure leaves *zero* Agent row behind (not "delete() was called" — the
|
|
15
|
+
* row is actually gone from the database), and that a Kubernetes
|
|
16
|
+
* provisioning failure still deletes the row even though provisioning runs
|
|
17
|
+
* after that transaction has already committed.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
21
|
+
import type { PrismaClient } from "../prisma/client/client.ts";
|
|
22
|
+
import { AgentCronJobService } from "./agent-cron-jobs.ts";
|
|
23
|
+
import { deleteAgentFully } from "./agent-deletion.ts";
|
|
24
|
+
import { AgentEnvService } from "./agent-envs.ts";
|
|
25
|
+
import { AgentMemberService } from "./agent-members.ts";
|
|
26
|
+
import { AgentPluginService } from "./agent-plugins.ts";
|
|
27
|
+
import type { AgentProvisioner, ProvisionResult } from "./agent-provisioner.ts";
|
|
28
|
+
import { AgentToolService } from "./agent-tools.ts";
|
|
29
|
+
import type { SpecialtyManifestResolver } from "./specialty-manifest-loader.ts";
|
|
30
|
+
import type { SpecialtyManifest } from "./specialty-registry.ts";
|
|
31
|
+
import type { CreateAgentDeps } from "./agents.ts";
|
|
32
|
+
import { AgentService, createAgent } from "./agents.ts";
|
|
33
|
+
import { createAdminPrismaClient } from "./prisma-client.ts";
|
|
34
|
+
import { makeTokenCrypto } from "./token-crypto.ts";
|
|
35
|
+
|
|
36
|
+
const TEST_DB = process.env.DATABASE_URL_ADMIN_TEST;
|
|
37
|
+
const describeOrSkip = TEST_DB ? describe : describe.skip;
|
|
38
|
+
|
|
39
|
+
function makePrisma(): PrismaClient {
|
|
40
|
+
// TEST_DB is guaranteed set — the describe block is skipped otherwise.
|
|
41
|
+
return createAdminPrismaClient(TEST_DB as string);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A minimal valid "coding" manifest, matching agents-api.integration.test.ts's fixture. */
|
|
45
|
+
const CODING_MANIFEST: SpecialtyManifest = {
|
|
46
|
+
apiVersion: "shipwright.dev/v1alpha1",
|
|
47
|
+
kind: "Specialty",
|
|
48
|
+
metadata: {
|
|
49
|
+
name: "coding",
|
|
50
|
+
displayName: "Coding Agent",
|
|
51
|
+
description: "test manifest",
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
skills: [],
|
|
54
|
+
},
|
|
55
|
+
identity: { templatesDir: "workspace/" },
|
|
56
|
+
crons: [],
|
|
57
|
+
plugins: ["shipwright"],
|
|
58
|
+
tools: ["Read", "Write", "Edit", "Bash"],
|
|
59
|
+
env: { required: [], optional: [] },
|
|
60
|
+
members: [],
|
|
61
|
+
repos: [],
|
|
62
|
+
chat: true,
|
|
63
|
+
voice: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function fakeSpecialtyRegistry(
|
|
67
|
+
byType: Record<string, SpecialtyManifest> = { coding: CODING_MANIFEST },
|
|
68
|
+
): SpecialtyManifestResolver {
|
|
69
|
+
return {
|
|
70
|
+
getManifest(typeName: string): SpecialtyManifest {
|
|
71
|
+
return byType[typeName] ?? (byType.coding as SpecialtyManifest);
|
|
72
|
+
},
|
|
73
|
+
tryGetManifest(typeName: string): SpecialtyManifest | undefined {
|
|
74
|
+
return byType[typeName];
|
|
75
|
+
},
|
|
76
|
+
listTypes() {
|
|
77
|
+
return Object.keys(byType).map((name) => ({ name, displayName: name }));
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeProvisioner(
|
|
83
|
+
overrides: Partial<AgentProvisioner> = {},
|
|
84
|
+
): AgentProvisioner {
|
|
85
|
+
return {
|
|
86
|
+
canProvision: false,
|
|
87
|
+
provision: async (): Promise<ProvisionResult> => ({
|
|
88
|
+
resourceName: "r",
|
|
89
|
+
secretName: "s",
|
|
90
|
+
deploymentName: "d",
|
|
91
|
+
}),
|
|
92
|
+
deprovision: async () => {},
|
|
93
|
+
reconcile: async () => ({
|
|
94
|
+
recreated: [],
|
|
95
|
+
migrated: [],
|
|
96
|
+
noOwner: [],
|
|
97
|
+
dryRun: false,
|
|
98
|
+
updated: [],
|
|
99
|
+
orphans: [],
|
|
100
|
+
failed: [],
|
|
101
|
+
}),
|
|
102
|
+
...overrides,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function baseInput(overrides: Partial<Parameters<typeof createAgent>[1]> = {}) {
|
|
107
|
+
return {
|
|
108
|
+
name: "Test Agent",
|
|
109
|
+
typeName: "coding",
|
|
110
|
+
runtime: undefined,
|
|
111
|
+
reposRaw: undefined,
|
|
112
|
+
authorAllowlistRaw: undefined,
|
|
113
|
+
patchAuthorAllowlistRaw: undefined,
|
|
114
|
+
memberEmailsRaw: undefined,
|
|
115
|
+
restrictSlackToMembersRaw: undefined,
|
|
116
|
+
claudeCodeOauthToken: undefined,
|
|
117
|
+
anthropicApiKey: undefined,
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describeOrSkip("createAgent() — integration (real Postgres)", () => {
|
|
123
|
+
let prisma: PrismaClient;
|
|
124
|
+
let agentService: AgentService;
|
|
125
|
+
let agentToolService: AgentToolService;
|
|
126
|
+
let agentPluginService: AgentPluginService;
|
|
127
|
+
let agentMemberService: AgentMemberService;
|
|
128
|
+
let agentEnvService: AgentEnvService;
|
|
129
|
+
let agentCronJobService: AgentCronJobService;
|
|
130
|
+
|
|
131
|
+
beforeEach(async () => {
|
|
132
|
+
prisma = makePrisma();
|
|
133
|
+
agentService = new AgentService(prisma, fakeSpecialtyRegistry());
|
|
134
|
+
agentToolService = new AgentToolService(prisma);
|
|
135
|
+
agentPluginService = new AgentPluginService(prisma);
|
|
136
|
+
agentMemberService = new AgentMemberService(prisma);
|
|
137
|
+
agentEnvService = new AgentEnvService(prisma, makeTokenCrypto());
|
|
138
|
+
// No crons in the fixture manifest, so reconcileSystemCrons() is a
|
|
139
|
+
// deliberate no-op here — its own atomicity is covered by
|
|
140
|
+
// agent-cron-jobs.unit.test.ts; this suite only needs it not to throw.
|
|
141
|
+
agentCronJobService = new AgentCronJobService(
|
|
142
|
+
prisma,
|
|
143
|
+
undefined,
|
|
144
|
+
fakeSpecialtyRegistry(),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Truncate between tests — this suite shares a DB with every other
|
|
148
|
+
// admin/src/*.integration.test.ts file (see docs/test-readiness/
|
|
149
|
+
// test-system.md's "real per-run DB with truncate-between-tests"
|
|
150
|
+
// convention, needed because several services already open their own
|
|
151
|
+
// interactive $transaction internally).
|
|
152
|
+
await prisma.agentTool.deleteMany();
|
|
153
|
+
await prisma.agentPlugin.deleteMany();
|
|
154
|
+
await prisma.agentMember.deleteMany();
|
|
155
|
+
await prisma.agentEnv.deleteMany();
|
|
156
|
+
await prisma.agentCronJob.deleteMany();
|
|
157
|
+
await prisma.agent.deleteMany();
|
|
158
|
+
await prisma.account.deleteMany();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(async () => {
|
|
162
|
+
await prisma.$disconnect();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
function deps(overrides: Partial<CreateAgentDeps> = {}): CreateAgentDeps {
|
|
166
|
+
return {
|
|
167
|
+
agentService,
|
|
168
|
+
agentToolService,
|
|
169
|
+
agentPluginService,
|
|
170
|
+
agentMemberService,
|
|
171
|
+
agentEnvService,
|
|
172
|
+
agentCronJobService,
|
|
173
|
+
provisioner: makeProvisioner(),
|
|
174
|
+
specialtyRegistry: fakeSpecialtyRegistry(),
|
|
175
|
+
...overrides,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
it("happy path: creates the Agent row, seeds manifest tools/plugins, and attaches repos/allowlists/members — matching today's form-handler end state", async () => {
|
|
180
|
+
const result = await createAgent(
|
|
181
|
+
deps(),
|
|
182
|
+
baseInput({
|
|
183
|
+
reposRaw: "org/repo1\norg/repo2",
|
|
184
|
+
authorAllowlistRaw: "octocat\nhubot",
|
|
185
|
+
patchAuthorAllowlistRaw: "patcher1",
|
|
186
|
+
memberEmailsRaw: "dev@example.com",
|
|
187
|
+
restrictSlackToMembersRaw: "true",
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(result.ok).toBe(true);
|
|
192
|
+
if (!result.ok) throw new Error("expected success");
|
|
193
|
+
expect(result.restrictSlackToMembers).toBe(true);
|
|
194
|
+
|
|
195
|
+
const dbAgent = await prisma.agent.findUnique({
|
|
196
|
+
where: { id: result.agent.id },
|
|
197
|
+
});
|
|
198
|
+
expect(dbAgent).not.toBeNull();
|
|
199
|
+
expect(dbAgent?.repos).toEqual(["org/repo1", "org/repo2"]);
|
|
200
|
+
expect(dbAgent?.reviewAuthorAllowlist).toEqual(["octocat", "hubot"]);
|
|
201
|
+
expect(dbAgent?.patchAuthorAllowlist).toEqual(["patcher1"]);
|
|
202
|
+
expect(dbAgent?.restrictSlackToMembers).toBe(true);
|
|
203
|
+
|
|
204
|
+
const tools = await prisma.agentTool.findMany({
|
|
205
|
+
where: { agentId: result.agent.id },
|
|
206
|
+
});
|
|
207
|
+
expect(tools.map((t) => t.pattern).sort()).toEqual(
|
|
208
|
+
[...CODING_MANIFEST.tools].sort(),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const plugins = await prisma.agentPlugin.findMany({
|
|
212
|
+
where: { agentId: result.agent.id },
|
|
213
|
+
});
|
|
214
|
+
expect(plugins.map((p) => p.name)).toEqual(["shipwright"]);
|
|
215
|
+
|
|
216
|
+
const members = await prisma.agentMember.findMany({
|
|
217
|
+
where: { agentId: result.agent.id },
|
|
218
|
+
});
|
|
219
|
+
expect(members.map((m) => m.email)).toEqual(["dev@example.com"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("a failure at the tool-seeding step leaves ZERO Agent row behind in the real database (single-transaction rollback)", async () => {
|
|
223
|
+
const failingAgentToolService = {
|
|
224
|
+
add: async (): Promise<never> => {
|
|
225
|
+
throw new Error("boom: tool seeding failed");
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = await createAgent(
|
|
230
|
+
deps({ agentToolService: failingAgentToolService }),
|
|
231
|
+
baseInput(),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(result).toEqual({ ok: false, errorCode: "seed_failed" });
|
|
235
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
236
|
+
expect(await prisma.agentTool.findMany()).toEqual([]);
|
|
237
|
+
expect(await prisma.agentPlugin.findMany()).toEqual([]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("a failure at the plugin-seeding step leaves ZERO Agent row behind, including any tools already seeded in the same transaction", async () => {
|
|
241
|
+
const failingAgentPluginService = {
|
|
242
|
+
add: async (): Promise<never> => {
|
|
243
|
+
throw new Error("boom: plugin seeding failed");
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = await createAgent(
|
|
248
|
+
deps({ agentPluginService: failingAgentPluginService }),
|
|
249
|
+
baseInput(),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(result).toEqual({ ok: false, errorCode: "seed_failed" });
|
|
253
|
+
// Tools were seeded before the plugin step failed — proving the whole
|
|
254
|
+
// sequence (not just the agent row) rolls back atomically.
|
|
255
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
256
|
+
expect(await prisma.agentTool.findMany()).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("invalid repo format leaves ZERO Agent row behind, including manifest-seeded tools/plugins", async () => {
|
|
260
|
+
const result = await createAgent(
|
|
261
|
+
deps(),
|
|
262
|
+
baseInput({ reposRaw: "not a valid repo!!" }),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(result).toEqual({ ok: false, errorCode: "invalid_repo_format" });
|
|
266
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
267
|
+
expect(await prisma.agentTool.findMany()).toEqual([]);
|
|
268
|
+
expect(await prisma.agentPlugin.findMany()).toEqual([]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("invalid author allowlist format leaves ZERO Agent row behind", async () => {
|
|
272
|
+
const result = await createAgent(
|
|
273
|
+
deps(),
|
|
274
|
+
baseInput({ authorAllowlistRaw: "octocat\nnot a valid login!" }),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(result).toEqual({
|
|
278
|
+
ok: false,
|
|
279
|
+
errorCode: "invalid_author_allowlist_format",
|
|
280
|
+
});
|
|
281
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("a Kubernetes provisioning failure still deletes the Agent row, even though provisioning runs after the DB transaction already committed", async () => {
|
|
285
|
+
const result = await createAgent(
|
|
286
|
+
deps({
|
|
287
|
+
provisioner: makeProvisioner({
|
|
288
|
+
canProvision: true,
|
|
289
|
+
provision: async () => {
|
|
290
|
+
throw new Error("boom: no cluster reachable");
|
|
291
|
+
},
|
|
292
|
+
}),
|
|
293
|
+
}),
|
|
294
|
+
baseInput({ runtime: "in-cluster" }),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(result).toEqual({ ok: false, errorCode: "provision_failed" });
|
|
298
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
299
|
+
// Tools/plugins were seeded (and committed) before provisioning ran —
|
|
300
|
+
// deleting the Agent row cascades and removes them too.
|
|
301
|
+
expect(await prisma.agentTool.findMany()).toEqual([]);
|
|
302
|
+
expect(await prisma.agentPlugin.findMany()).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("provisioning_disabled: requesting runtime=in-cluster with a provisioner that can't provision creates zero rows", async () => {
|
|
306
|
+
const result = await createAgent(
|
|
307
|
+
deps({ provisioner: makeProvisioner({ canProvision: false }) }),
|
|
308
|
+
baseInput({ runtime: "in-cluster" }),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(result).toEqual({ ok: false, errorCode: "provisioning_disabled" });
|
|
312
|
+
expect(await prisma.agent.findMany()).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("a reconcileSystemCrons() failure is non-fatal — the agent row survives and creation still succeeds", async () => {
|
|
316
|
+
const failingAgentCronJobService = {
|
|
317
|
+
reconcileSystemCrons: async (): Promise<never> => {
|
|
318
|
+
throw new Error("boom: cron reconcile failed");
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const result = await createAgent(
|
|
323
|
+
deps({ agentCronJobService: failingAgentCronJobService }),
|
|
324
|
+
baseInput(),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
expect(result.ok).toBe(true);
|
|
328
|
+
if (!result.ok) throw new Error("expected success");
|
|
329
|
+
expect(
|
|
330
|
+
await prisma.agent.findUnique({ where: { id: result.agent.id } }),
|
|
331
|
+
).not.toBeNull();
|
|
332
|
+
});
|
|
333
|
+
describe("account quota (SSP-4.1)", () => {
|
|
334
|
+
async function makeAccount(
|
|
335
|
+
overrides: { status?: string; maxAgents?: number } = {},
|
|
336
|
+
) {
|
|
337
|
+
return prisma.account.create({
|
|
338
|
+
data: { name: "Acme", status: "active", maxAgents: 2, ...overrides },
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
it("allows creates up to maxAgents then fails with quota_exceeded, writing nothing", async () => {
|
|
343
|
+
const account = await makeAccount({ maxAgents: 2 });
|
|
344
|
+
await prisma.agent.create({
|
|
345
|
+
data: { name: "existing", accountId: account.id },
|
|
346
|
+
});
|
|
347
|
+
const provisioned: string[] = [];
|
|
348
|
+
const d = deps({
|
|
349
|
+
provisioner: makeProvisioner({
|
|
350
|
+
canProvision: true,
|
|
351
|
+
provision: async (id) => {
|
|
352
|
+
provisioned.push(id);
|
|
353
|
+
return { resourceName: "r", secretName: "s", deploymentName: "d" };
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const second = await createAgent(
|
|
359
|
+
d,
|
|
360
|
+
baseInput({
|
|
361
|
+
name: "second",
|
|
362
|
+
runtime: "in-cluster",
|
|
363
|
+
accountId: account.id,
|
|
364
|
+
}),
|
|
365
|
+
);
|
|
366
|
+
expect(second.ok).toBe(true);
|
|
367
|
+
expect(provisioned).toHaveLength(1);
|
|
368
|
+
|
|
369
|
+
const third = await createAgent(
|
|
370
|
+
d,
|
|
371
|
+
baseInput({
|
|
372
|
+
name: "third",
|
|
373
|
+
runtime: "in-cluster",
|
|
374
|
+
accountId: account.id,
|
|
375
|
+
claudeCodeOauthToken: "tok",
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
expect(third).toEqual({ ok: false, errorCode: "quota_exceeded" });
|
|
379
|
+
expect(provisioned).toHaveLength(1);
|
|
380
|
+
expect(
|
|
381
|
+
await prisma.agent.count({ where: { accountId: account.id } }),
|
|
382
|
+
).toBe(2);
|
|
383
|
+
expect(
|
|
384
|
+
await prisma.agent.findFirst({ where: { name: "third" } }),
|
|
385
|
+
).toBeNull();
|
|
386
|
+
expect(
|
|
387
|
+
await prisma.agentEnv.findMany({ where: { agent: { name: "third" } } }),
|
|
388
|
+
).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("concurrent creates with one slot left produce exactly one agent", async () => {
|
|
392
|
+
const account = await makeAccount({ maxAgents: 1 });
|
|
393
|
+
|
|
394
|
+
const results = await Promise.all(
|
|
395
|
+
["a", "b", "c", "d"].map((n) =>
|
|
396
|
+
createAgent(deps(), baseInput({ name: n, accountId: account.id })),
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(results.filter((r) => r.ok)).toHaveLength(1);
|
|
401
|
+
expect(
|
|
402
|
+
results.filter((r) => !r.ok && r.errorCode === "quota_exceeded"),
|
|
403
|
+
).toHaveLength(3);
|
|
404
|
+
expect(
|
|
405
|
+
await prisma.agent.count({ where: { accountId: account.id } }),
|
|
406
|
+
).toBe(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it.each(["suspended", "trial_expired"])(
|
|
410
|
+
"fails with account_inactive for a %s account",
|
|
411
|
+
async (status) => {
|
|
412
|
+
const account = await makeAccount({ status, maxAgents: 5 });
|
|
413
|
+
const result = await createAgent(
|
|
414
|
+
deps(),
|
|
415
|
+
baseInput({ accountId: account.id }),
|
|
416
|
+
);
|
|
417
|
+
expect(result).toEqual({ ok: false, errorCode: "account_inactive" });
|
|
418
|
+
expect(await prisma.agent.count()).toBe(0);
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
it("fails with account_inactive for an unknown account id", async () => {
|
|
423
|
+
const result = await createAgent(
|
|
424
|
+
deps(),
|
|
425
|
+
baseInput({ accountId: "nope" }),
|
|
426
|
+
);
|
|
427
|
+
expect(result).toEqual({ ok: false, errorCode: "account_inactive" });
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("a provision_failed rollback leaves the account's agent count unchanged and frees the slot", async () => {
|
|
431
|
+
const account = await makeAccount({ maxAgents: 1 });
|
|
432
|
+
const failing = deps({
|
|
433
|
+
provisioner: makeProvisioner({
|
|
434
|
+
canProvision: true,
|
|
435
|
+
provision: async () => {
|
|
436
|
+
throw new Error("k8s down");
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const failed = await createAgent(
|
|
442
|
+
failing,
|
|
443
|
+
baseInput({ runtime: "in-cluster", accountId: account.id }),
|
|
444
|
+
);
|
|
445
|
+
expect(failed).toEqual({ ok: false, errorCode: "provision_failed" });
|
|
446
|
+
expect(
|
|
447
|
+
await prisma.agent.count({ where: { accountId: account.id } }),
|
|
448
|
+
).toBe(0);
|
|
449
|
+
|
|
450
|
+
const retry = await createAgent(
|
|
451
|
+
deps(),
|
|
452
|
+
baseInput({ accountId: account.id }),
|
|
453
|
+
);
|
|
454
|
+
expect(retry.ok).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("create-delete-create cycle: deleting via deleteAgentFully frees the quota slot (SSP-4.2)", async () => {
|
|
458
|
+
const account = await makeAccount({ maxAgents: 1 });
|
|
459
|
+
const first = await createAgent(
|
|
460
|
+
deps(),
|
|
461
|
+
baseInput({ name: "first", accountId: account.id }),
|
|
462
|
+
);
|
|
463
|
+
expect(first.ok).toBe(true);
|
|
464
|
+
if (!first.ok) throw new Error("expected success");
|
|
465
|
+
|
|
466
|
+
const blocked = await createAgent(
|
|
467
|
+
deps(),
|
|
468
|
+
baseInput({ name: "second", accountId: account.id }),
|
|
469
|
+
);
|
|
470
|
+
expect(blocked).toEqual({ ok: false, errorCode: "quota_exceeded" });
|
|
471
|
+
|
|
472
|
+
const deleted = await deleteAgentFully(first.agent.id, {
|
|
473
|
+
prisma,
|
|
474
|
+
provisioner: { deprovision: async () => {} },
|
|
475
|
+
serviceIntegrations: [
|
|
476
|
+
{
|
|
477
|
+
name: "task-store",
|
|
478
|
+
listTokensForAgent: async () => [],
|
|
479
|
+
revokeToken: async () => {},
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
chatService: {
|
|
483
|
+
listTokensForAgent: async () => [],
|
|
484
|
+
revokeToken: async () => {},
|
|
485
|
+
deleteThreadsForAgent: async () => ({ deleted: 0 }),
|
|
486
|
+
},
|
|
487
|
+
slack: { deleteApp: async () => {} },
|
|
488
|
+
decrypt: (v) => v,
|
|
489
|
+
});
|
|
490
|
+
expect(deleted.agentDeleted).toBe(true);
|
|
491
|
+
expect(
|
|
492
|
+
await prisma.agent.count({ where: { accountId: account.id } }),
|
|
493
|
+
).toBe(0);
|
|
494
|
+
|
|
495
|
+
const again = await createAgent(
|
|
496
|
+
deps(),
|
|
497
|
+
baseInput({ name: "second", accountId: account.id }),
|
|
498
|
+
);
|
|
499
|
+
expect(again.ok).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("agents created without accountId are unaffected", async () => {
|
|
503
|
+
const result = await createAgent(deps(), baseInput());
|
|
504
|
+
expect(result.ok).toBe(true);
|
|
505
|
+
if (!result.ok) throw new Error("expected success");
|
|
506
|
+
const row = await prisma.agent.findUnique({
|
|
507
|
+
where: { id: result.agent.id },
|
|
508
|
+
});
|
|
509
|
+
expect(row?.accountId).toBeNull();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|