@app-vitals/agent-admin 0.0.0-stage → 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 +64 -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,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/slack-provisioning-service.integration.test.ts
|
|
3
|
+
* Integration tests for SlackProvisioningService (UAP-1.1) — the extracted
|
|
4
|
+
* Slack app-manifest-creation/OAuth/app-token orchestration.
|
|
5
|
+
*
|
|
6
|
+
* Uses the same RecordedSlackClient + cassette fixture pattern as
|
|
7
|
+
* slack-provisioning.integration.test.ts (real dependency behavior via a
|
|
8
|
+
* recorded fixture double), but exercises the service directly rather than
|
|
9
|
+
* going through the Hono app.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { sign } from "hono/jwt";
|
|
15
|
+
import type { AdminUISlackClient } from "./admin-ui.ts";
|
|
16
|
+
import type { AgentDetail } from "./agents.ts";
|
|
17
|
+
import { SlackProvisioningService } from "./slack-provisioning-service.ts";
|
|
18
|
+
import type { AppManifest } from "./slack-provisioning-client.ts";
|
|
19
|
+
|
|
20
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const SESSION_SECRET = "test-admin-session-secret-32-bytes!";
|
|
23
|
+
const AGENT_ID = "agent-test-123";
|
|
24
|
+
const APP_BASE_URL = "https://example.com";
|
|
25
|
+
/** The per-agent connect-slack callback the new UAP-1.1 route registers with Slack. */
|
|
26
|
+
const CONNECT_SLACK_CALLBACK = `${APP_BASE_URL}/admin/agents/${AGENT_ID}/connect-slack/callback`;
|
|
27
|
+
/** The legacy wizard's OAuth completion route. */
|
|
28
|
+
const LEGACY_PROVISION_COMPLETE = `${APP_BASE_URL}/admin/provision/complete`;
|
|
29
|
+
|
|
30
|
+
// ─── Cassette fixture ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface ProvisionCassette {
|
|
33
|
+
createAppManifest: {
|
|
34
|
+
appId: string;
|
|
35
|
+
oauthRedirectUrl: string;
|
|
36
|
+
clientId: string;
|
|
37
|
+
clientSecret: string;
|
|
38
|
+
signingSecret: string;
|
|
39
|
+
};
|
|
40
|
+
authTest: {
|
|
41
|
+
userId: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class RecordedSlackClient implements AdminUISlackClient {
|
|
46
|
+
private cassette: ProvisionCassette;
|
|
47
|
+
/** Manifests passed to createAppManifest(), in call order — lets tests assert on the registered OAuth redirect_urls. */
|
|
48
|
+
readonly createdManifests: AppManifest[] = [];
|
|
49
|
+
/** redirect_uri values passed to exchangeOAuthCode(), in call order. */
|
|
50
|
+
readonly exchangeRedirectUris: string[] = [];
|
|
51
|
+
/** botToken values passed to authTest(), in call order. */
|
|
52
|
+
readonly authTestBotTokens: string[] = [];
|
|
53
|
+
|
|
54
|
+
constructor(cassettePath: string) {
|
|
55
|
+
this.cassette = JSON.parse(readFileSync(cassettePath, "utf-8"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async createAppManifest(
|
|
59
|
+
_xoxpToken: string,
|
|
60
|
+
manifest: AppManifest,
|
|
61
|
+
): Promise<{
|
|
62
|
+
appId: string;
|
|
63
|
+
oauthRedirectUrl: string;
|
|
64
|
+
clientId: string;
|
|
65
|
+
clientSecret: string;
|
|
66
|
+
signingSecret: string;
|
|
67
|
+
}> {
|
|
68
|
+
this.createdManifests.push(manifest);
|
|
69
|
+
return this.cassette.createAppManifest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async updateAppManifest(
|
|
73
|
+
_xoxpToken: string,
|
|
74
|
+
_appId: string,
|
|
75
|
+
_manifest: AppManifest,
|
|
76
|
+
): Promise<void> {}
|
|
77
|
+
|
|
78
|
+
async exchangeOAuthCode(
|
|
79
|
+
_code: string,
|
|
80
|
+
_clientId: string,
|
|
81
|
+
_clientSecret: string,
|
|
82
|
+
redirectUri: string,
|
|
83
|
+
): Promise<{ botToken: string }> {
|
|
84
|
+
this.exchangeRedirectUris.push(redirectUri);
|
|
85
|
+
return { botToken: "xoxb-test-cassette-bot-token" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async authTest(botToken: string): Promise<{ userId: string }> {
|
|
89
|
+
this.authTestBotTokens.push(botToken);
|
|
90
|
+
return this.cassette.authTest;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const CASSETTE_PATH = new URL(
|
|
95
|
+
"./fixtures/slack-provision-cassette.json",
|
|
96
|
+
import.meta.url,
|
|
97
|
+
).pathname;
|
|
98
|
+
|
|
99
|
+
// ─── Test doubles ─────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
interface PatchCall {
|
|
102
|
+
agentId: string;
|
|
103
|
+
env: Record<string, string>;
|
|
104
|
+
secretKeys: string[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface UpdateFieldsCall {
|
|
108
|
+
id: string;
|
|
109
|
+
input: unknown;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makeService(opts?: {
|
|
113
|
+
slackClient?: AdminUISlackClient;
|
|
114
|
+
patchCalls?: PatchCall[];
|
|
115
|
+
reconcileCalls?: string[];
|
|
116
|
+
updateFieldsCalls?: UpdateFieldsCall[];
|
|
117
|
+
getConfigBundle?: () => Promise<{
|
|
118
|
+
env: Record<string, string>;
|
|
119
|
+
agentId: string;
|
|
120
|
+
allowedTools: string[];
|
|
121
|
+
} | null>;
|
|
122
|
+
getDetail?: () => Promise<AgentDetail | null>;
|
|
123
|
+
}): SlackProvisioningService {
|
|
124
|
+
const patchCalls = opts?.patchCalls ?? [];
|
|
125
|
+
const reconcileCalls = opts?.reconcileCalls ?? [];
|
|
126
|
+
const updateFieldsCalls = opts?.updateFieldsCalls ?? [];
|
|
127
|
+
return new SlackProvisioningService({
|
|
128
|
+
slackClient: opts?.slackClient ?? new RecordedSlackClient(CASSETTE_PATH),
|
|
129
|
+
agentService: {
|
|
130
|
+
getDetail:
|
|
131
|
+
opts?.getDetail ??
|
|
132
|
+
(async () => ({
|
|
133
|
+
id: AGENT_ID,
|
|
134
|
+
name: "Test Agent",
|
|
135
|
+
slackId: "U123456",
|
|
136
|
+
selfHosted: false,
|
|
137
|
+
createdAt: new Date("2024-01-01"),
|
|
138
|
+
updatedAt: new Date("2024-01-01"),
|
|
139
|
+
repos: [],
|
|
140
|
+
reviewAuthorAllowlist: [],
|
|
141
|
+
patchAuthorAllowlist: [],
|
|
142
|
+
restrictSlackToMembers: false,
|
|
143
|
+
typeName: "coding",
|
|
144
|
+
missingRequiredEnv: [],
|
|
145
|
+
})),
|
|
146
|
+
updateFields: async (id: string, input: unknown) => {
|
|
147
|
+
updateFieldsCalls.push({ id, input });
|
|
148
|
+
return {
|
|
149
|
+
id,
|
|
150
|
+
name: "Test Agent",
|
|
151
|
+
slackId: "U123456",
|
|
152
|
+
selfHosted: false,
|
|
153
|
+
createdAt: new Date("2024-01-01"),
|
|
154
|
+
updatedAt: new Date("2024-01-01"),
|
|
155
|
+
repos: [],
|
|
156
|
+
reviewAuthorAllowlist: [],
|
|
157
|
+
patchAuthorAllowlist: [],
|
|
158
|
+
restrictSlackToMembers: false,
|
|
159
|
+
typeName: "coding",
|
|
160
|
+
missingRequiredEnv: [],
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
agentEnvService: {
|
|
165
|
+
getConfigBundle: opts?.getConfigBundle ?? (async () => null),
|
|
166
|
+
patch: async (
|
|
167
|
+
agentId: string,
|
|
168
|
+
env: Record<string, string>,
|
|
169
|
+
secretKeys?: Set<string>,
|
|
170
|
+
) => {
|
|
171
|
+
patchCalls.push({
|
|
172
|
+
agentId,
|
|
173
|
+
env,
|
|
174
|
+
secretKeys: secretKeys ? [...secretKeys] : [],
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
agentCronJobService: {
|
|
179
|
+
reconcileSystemCrons: async (agentId: string) => {
|
|
180
|
+
reconcileCalls.push(agentId);
|
|
181
|
+
return { created: 3, updated: 0, deleted: 0 };
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
sessionSecret: SESSION_SECRET,
|
|
185
|
+
appBaseUrl: APP_BASE_URL,
|
|
186
|
+
secretEnvVars: new Set(["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function makeProvisionStateToken(
|
|
191
|
+
overrides: Partial<{
|
|
192
|
+
agentId: string;
|
|
193
|
+
clientId: string;
|
|
194
|
+
clientSecret: string;
|
|
195
|
+
signingSecret: string;
|
|
196
|
+
appId: string;
|
|
197
|
+
redirectUri: string;
|
|
198
|
+
/** When true, omit redirectUri entirely (simulates a cookie signed before the field existed). */
|
|
199
|
+
omitRedirectUri: boolean;
|
|
200
|
+
expired: boolean;
|
|
201
|
+
secret: string;
|
|
202
|
+
}> = {},
|
|
203
|
+
): Promise<string> {
|
|
204
|
+
const now = Math.floor(Date.now() / 1000);
|
|
205
|
+
return sign(
|
|
206
|
+
{
|
|
207
|
+
agentId: overrides.agentId ?? AGENT_ID,
|
|
208
|
+
clientId: overrides.clientId ?? "test-client-id",
|
|
209
|
+
clientSecret: overrides.clientSecret ?? "test-client-secret",
|
|
210
|
+
signingSecret: overrides.signingSecret ?? "test-signing-secret",
|
|
211
|
+
appId: overrides.appId ?? "A0123456789",
|
|
212
|
+
...(overrides.omitRedirectUri
|
|
213
|
+
? {}
|
|
214
|
+
: { redirectUri: overrides.redirectUri ?? CONNECT_SLACK_CALLBACK }),
|
|
215
|
+
iat: now,
|
|
216
|
+
exp: overrides.expired ? now - 10 : now + 300,
|
|
217
|
+
},
|
|
218
|
+
overrides.secret ?? SESSION_SECRET,
|
|
219
|
+
"HS256",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe("SlackProvisioningService.startConnect", () => {
|
|
226
|
+
it("valid xoxe.xoxp- token → calls apps.manifest.create (cassette) and returns a signed provision-state token + oauth URL", async () => {
|
|
227
|
+
const service = makeService();
|
|
228
|
+
const result = await service.startConnect(
|
|
229
|
+
AGENT_ID,
|
|
230
|
+
"xoxe.xoxp-1-fake-token-for-testing",
|
|
231
|
+
CONNECT_SLACK_CALLBACK,
|
|
232
|
+
);
|
|
233
|
+
expect(result.ok).toBe(true);
|
|
234
|
+
if (!result.ok) throw new Error("expected ok result");
|
|
235
|
+
expect(result.oauthRedirectUrl).toContain(
|
|
236
|
+
"https://slack.com/oauth/authorize",
|
|
237
|
+
);
|
|
238
|
+
expect(typeof result.provisionStateToken).toBe("string");
|
|
239
|
+
expect(result.provisionStateToken.length).toBeGreaterThan(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("registers the caller-supplied redirectUri in the app manifest's OAuth redirect_urls (per-agent connect-slack flow), and threads it into the signed state so the OAuth exchange reuses the same URL", async () => {
|
|
243
|
+
const slackClient = new RecordedSlackClient(CASSETTE_PATH);
|
|
244
|
+
const service = makeService({ slackClient });
|
|
245
|
+
|
|
246
|
+
const result = await service.startConnect(
|
|
247
|
+
AGENT_ID,
|
|
248
|
+
"xoxe.xoxp-1-fake-token-for-testing",
|
|
249
|
+
CONNECT_SLACK_CALLBACK,
|
|
250
|
+
);
|
|
251
|
+
expect(result.ok).toBe(true);
|
|
252
|
+
if (!result.ok) throw new Error("expected ok result");
|
|
253
|
+
|
|
254
|
+
// The manifest registered with Slack must carry the per-agent callback,
|
|
255
|
+
// NOT the legacy /admin/provision/complete route.
|
|
256
|
+
expect(slackClient.createdManifests.length).toBe(1);
|
|
257
|
+
expect(slackClient.createdManifests[0].oauth_config?.redirect_urls).toEqual(
|
|
258
|
+
[CONNECT_SLACK_CALLBACK],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// And the OAuth exchange must use the identical URL, recovered from the
|
|
262
|
+
// signed state cookie — Slack rejects a mismatch. Prove it by completing
|
|
263
|
+
// with the token this call produced.
|
|
264
|
+
const completeResult = await service.completeConnect(
|
|
265
|
+
result.provisionStateToken,
|
|
266
|
+
"valid-oauth-code",
|
|
267
|
+
// Fallback would be the wrong (legacy) URL — the state's own value must win.
|
|
268
|
+
LEGACY_PROVISION_COMPLETE,
|
|
269
|
+
);
|
|
270
|
+
expect(completeResult.outcome).toBe("needs_app_token");
|
|
271
|
+
expect(slackClient.exchangeRedirectUris).toEqual([CONNECT_SLACK_CALLBACK]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("a combined-flow ghConnectError (UAP-2.1) is folded into the signed state and handed back by completeConnect so the post-callback landing can surface it", async () => {
|
|
275
|
+
const service = makeService();
|
|
276
|
+
const result = await service.startConnect(
|
|
277
|
+
AGENT_ID,
|
|
278
|
+
"xoxe.xoxp-1-fake-token-for-testing",
|
|
279
|
+
CONNECT_SLACK_CALLBACK,
|
|
280
|
+
"GitHub Personal Access Token is required.",
|
|
281
|
+
);
|
|
282
|
+
expect(result.ok).toBe(true);
|
|
283
|
+
if (!result.ok) throw new Error("expected ok result");
|
|
284
|
+
|
|
285
|
+
// The GitHub PAT failure rode through the signed state cookie and comes
|
|
286
|
+
// back out on the success outcome — never silently swallowed.
|
|
287
|
+
const completeResult = await service.completeConnect(
|
|
288
|
+
result.provisionStateToken,
|
|
289
|
+
"valid-oauth-code",
|
|
290
|
+
CONNECT_SLACK_CALLBACK,
|
|
291
|
+
);
|
|
292
|
+
expect(completeResult.outcome).toBe("needs_app_token");
|
|
293
|
+
if (completeResult.outcome !== "needs_app_token")
|
|
294
|
+
throw new Error("wrong outcome");
|
|
295
|
+
expect(completeResult.ghConnectError).toBe(
|
|
296
|
+
"GitHub Personal Access Token is required.",
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("no ghConnectError → completeConnect returns undefined (default flow unaffected)", async () => {
|
|
301
|
+
const service = makeService();
|
|
302
|
+
const result = await service.startConnect(
|
|
303
|
+
AGENT_ID,
|
|
304
|
+
"xoxe.xoxp-1-fake-token-for-testing",
|
|
305
|
+
CONNECT_SLACK_CALLBACK,
|
|
306
|
+
);
|
|
307
|
+
expect(result.ok).toBe(true);
|
|
308
|
+
if (!result.ok) throw new Error("expected ok result");
|
|
309
|
+
|
|
310
|
+
const completeResult = await service.completeConnect(
|
|
311
|
+
result.provisionStateToken,
|
|
312
|
+
"valid-oauth-code",
|
|
313
|
+
CONNECT_SLACK_CALLBACK,
|
|
314
|
+
);
|
|
315
|
+
expect(completeResult.outcome).toBe("needs_app_token");
|
|
316
|
+
if (completeResult.outcome !== "needs_app_token")
|
|
317
|
+
throw new Error("wrong outcome");
|
|
318
|
+
expect(completeResult.ghConnectError).toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("missing xoxpToken → ok: false with validation error, no Slack call", async () => {
|
|
322
|
+
const service = makeService();
|
|
323
|
+
const result = await service.startConnect(
|
|
324
|
+
AGENT_ID,
|
|
325
|
+
undefined,
|
|
326
|
+
CONNECT_SLACK_CALLBACK,
|
|
327
|
+
);
|
|
328
|
+
expect(result.ok).toBe(false);
|
|
329
|
+
if (result.ok) throw new Error("expected error result");
|
|
330
|
+
expect(result.error).toContain("xoxe.xoxp-");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("xoxpToken missing xoxe.xoxp- prefix → ok: false with validation error", async () => {
|
|
334
|
+
const service = makeService();
|
|
335
|
+
const result = await service.startConnect(
|
|
336
|
+
AGENT_ID,
|
|
337
|
+
"xoxb-not-a-user-token",
|
|
338
|
+
CONNECT_SLACK_CALLBACK,
|
|
339
|
+
);
|
|
340
|
+
expect(result.ok).toBe(false);
|
|
341
|
+
if (result.ok) throw new Error("expected error result");
|
|
342
|
+
expect(result.error).toContain("xoxe.xoxp-");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("agent id that doesn't exist → ok: false with 'Agent not found.'", async () => {
|
|
346
|
+
const service = makeService({ getDetail: async () => null });
|
|
347
|
+
const result = await service.startConnect(
|
|
348
|
+
"nonexistent-agent",
|
|
349
|
+
"xoxe.xoxp-1-fake-token-for-testing",
|
|
350
|
+
CONNECT_SLACK_CALLBACK,
|
|
351
|
+
);
|
|
352
|
+
expect(result.ok).toBe(false);
|
|
353
|
+
if (result.ok) throw new Error("expected error result");
|
|
354
|
+
expect(result.error).toBe("Agent not found.");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("SlackProvisioningService.completeConnect", () => {
|
|
359
|
+
it("valid state token + code → exchanges OAuth code (cassette) and stores SLACK_BOT_TOKEN, returns needs_app_token", async () => {
|
|
360
|
+
const patchCalls: PatchCall[] = [];
|
|
361
|
+
const updateFieldsCalls: UpdateFieldsCall[] = [];
|
|
362
|
+
const service = makeService({ patchCalls, updateFieldsCalls });
|
|
363
|
+
const token = await makeProvisionStateToken();
|
|
364
|
+
|
|
365
|
+
const result = await service.completeConnect(
|
|
366
|
+
token,
|
|
367
|
+
"valid-oauth-code",
|
|
368
|
+
CONNECT_SLACK_CALLBACK,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(result.outcome).toBe("needs_app_token");
|
|
372
|
+
if (result.outcome !== "needs_app_token") throw new Error("wrong outcome");
|
|
373
|
+
expect(result.agentId).toBe(AGENT_ID);
|
|
374
|
+
|
|
375
|
+
expect(patchCalls.length).toBe(1);
|
|
376
|
+
expect(patchCalls[0].env.SLACK_BOT_TOKEN).toBe(
|
|
377
|
+
"xoxb-test-cassette-bot-token",
|
|
378
|
+
);
|
|
379
|
+
expect(patchCalls[0].secretKeys).toContain("SLACK_BOT_TOKEN");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("valid state token + code → resolves the bot's own user id via auth.test (cassette) and persists it via agentService.updateFields", async () => {
|
|
383
|
+
const slackClient = new RecordedSlackClient(CASSETTE_PATH);
|
|
384
|
+
const updateFieldsCalls: UpdateFieldsCall[] = [];
|
|
385
|
+
const service = makeService({ slackClient, updateFieldsCalls });
|
|
386
|
+
const token = await makeProvisionStateToken();
|
|
387
|
+
|
|
388
|
+
const result = await service.completeConnect(
|
|
389
|
+
token,
|
|
390
|
+
"valid-oauth-code",
|
|
391
|
+
CONNECT_SLACK_CALLBACK,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(result.outcome).toBe("needs_app_token");
|
|
395
|
+
|
|
396
|
+
// auth.test is called with the freshly-exchanged bot token.
|
|
397
|
+
expect(slackClient.authTestBotTokens).toEqual([
|
|
398
|
+
"xoxb-test-cassette-bot-token",
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
// The resolved user id (from the cassette) is persisted to agent.slackId.
|
|
402
|
+
expect(updateFieldsCalls.length).toBe(1);
|
|
403
|
+
expect(updateFieldsCalls[0].id).toBe(AGENT_ID);
|
|
404
|
+
expect(updateFieldsCalls[0].input).toEqual({ slackId: "U0AALR8M69X" });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("reinstall (SLACK_APP_TOKEN already set) → returns reinstalled outcome, still resolves and persists slackId via auth.test", async () => {
|
|
408
|
+
const patchCalls: PatchCall[] = [];
|
|
409
|
+
const updateFieldsCalls: UpdateFieldsCall[] = [];
|
|
410
|
+
const service = makeService({
|
|
411
|
+
patchCalls,
|
|
412
|
+
updateFieldsCalls,
|
|
413
|
+
getConfigBundle: async () => ({
|
|
414
|
+
env: {
|
|
415
|
+
SLACK_BOT_TOKEN: "xoxb-old",
|
|
416
|
+
SLACK_APP_TOKEN: "xapp-existing",
|
|
417
|
+
},
|
|
418
|
+
agentId: AGENT_ID,
|
|
419
|
+
allowedTools: [],
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
const token = await makeProvisionStateToken();
|
|
423
|
+
|
|
424
|
+
const result = await service.completeConnect(
|
|
425
|
+
token,
|
|
426
|
+
"valid-oauth-code",
|
|
427
|
+
CONNECT_SLACK_CALLBACK,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(result.outcome).toBe("reinstalled");
|
|
431
|
+
if (result.outcome !== "reinstalled") throw new Error("wrong outcome");
|
|
432
|
+
expect(result.agentId).toBe(AGENT_ID);
|
|
433
|
+
|
|
434
|
+
expect(updateFieldsCalls.length).toBe(1);
|
|
435
|
+
expect(updateFieldsCalls[0].id).toBe(AGENT_ID);
|
|
436
|
+
expect(updateFieldsCalls[0].input).toEqual({ slackId: "U0AALR8M69X" });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("missing state cookie → invalid_state outcome, no Slack call, no env write", async () => {
|
|
440
|
+
const patchCalls: PatchCall[] = [];
|
|
441
|
+
const service = makeService({ patchCalls });
|
|
442
|
+
|
|
443
|
+
const result = await service.completeConnect(
|
|
444
|
+
undefined,
|
|
445
|
+
"some-code",
|
|
446
|
+
CONNECT_SLACK_CALLBACK,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
expect(result.outcome).toBe("invalid_state");
|
|
450
|
+
expect(patchCalls.length).toBe(0);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("expired state token → invalid_state outcome", async () => {
|
|
454
|
+
const patchCalls: PatchCall[] = [];
|
|
455
|
+
const service = makeService({ patchCalls });
|
|
456
|
+
const token = await makeProvisionStateToken({ expired: true });
|
|
457
|
+
|
|
458
|
+
const result = await service.completeConnect(
|
|
459
|
+
token,
|
|
460
|
+
"some-code",
|
|
461
|
+
CONNECT_SLACK_CALLBACK,
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(result.outcome).toBe("invalid_state");
|
|
465
|
+
expect(patchCalls.length).toBe(0);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("valid state token but missing code (OAuth denied) → missing_code outcome, no env write", async () => {
|
|
469
|
+
const patchCalls: PatchCall[] = [];
|
|
470
|
+
const service = makeService({ patchCalls });
|
|
471
|
+
const token = await makeProvisionStateToken();
|
|
472
|
+
|
|
473
|
+
const result = await service.completeConnect(
|
|
474
|
+
token,
|
|
475
|
+
undefined,
|
|
476
|
+
CONNECT_SLACK_CALLBACK,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
expect(result.outcome).toBe("missing_code");
|
|
480
|
+
expect(patchCalls.length).toBe(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("legacy wizard flow: state carries /admin/provision/complete → OAuth exchange uses that URL, not the per-agent callback", async () => {
|
|
484
|
+
const slackClient = new RecordedSlackClient(CASSETTE_PATH);
|
|
485
|
+
const service = makeService({ slackClient });
|
|
486
|
+
const token = await makeProvisionStateToken({
|
|
487
|
+
redirectUri: LEGACY_PROVISION_COMPLETE,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const result = await service.completeConnect(
|
|
491
|
+
token,
|
|
492
|
+
"valid-oauth-code",
|
|
493
|
+
LEGACY_PROVISION_COMPLETE,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(result.outcome).toBe("needs_app_token");
|
|
497
|
+
expect(slackClient.exchangeRedirectUris).toEqual([
|
|
498
|
+
LEGACY_PROVISION_COMPLETE,
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("state cookie predating the redirectUri field → OAuth exchange falls back to the caller-supplied URL", async () => {
|
|
503
|
+
const slackClient = new RecordedSlackClient(CASSETTE_PATH);
|
|
504
|
+
const service = makeService({ slackClient });
|
|
505
|
+
const token = await makeProvisionStateToken({ omitRedirectUri: true });
|
|
506
|
+
|
|
507
|
+
const result = await service.completeConnect(
|
|
508
|
+
token,
|
|
509
|
+
"valid-oauth-code",
|
|
510
|
+
LEGACY_PROVISION_COMPLETE,
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(result.outcome).toBe("needs_app_token");
|
|
514
|
+
expect(slackClient.exchangeRedirectUris).toEqual([
|
|
515
|
+
LEGACY_PROVISION_COMPLETE,
|
|
516
|
+
]);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("OAuth exchange failure → exchange_failed outcome", async () => {
|
|
520
|
+
const patchCalls: PatchCall[] = [];
|
|
521
|
+
const failingSlackClient: AdminUISlackClient = {
|
|
522
|
+
createAppManifest: async () => {
|
|
523
|
+
throw new Error("unused in this test");
|
|
524
|
+
},
|
|
525
|
+
updateAppManifest: async () => {},
|
|
526
|
+
exchangeOAuthCode: async () => {
|
|
527
|
+
throw new Error("invalid_code");
|
|
528
|
+
},
|
|
529
|
+
authTest: async () => {
|
|
530
|
+
throw new Error("unused in this test");
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
const service = makeService({
|
|
534
|
+
slackClient: failingSlackClient,
|
|
535
|
+
patchCalls,
|
|
536
|
+
});
|
|
537
|
+
const token = await makeProvisionStateToken();
|
|
538
|
+
|
|
539
|
+
const result = await service.completeConnect(
|
|
540
|
+
token,
|
|
541
|
+
"bad-code",
|
|
542
|
+
CONNECT_SLACK_CALLBACK,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
expect(result.outcome).toBe("exchange_failed");
|
|
546
|
+
if (result.outcome !== "exchange_failed") throw new Error("wrong outcome");
|
|
547
|
+
expect(result.error).toContain("invalid_code");
|
|
548
|
+
expect(patchCalls.length).toBe(0);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("auth.test failure after a successful exchange → exchange_failed outcome, SLACK_BOT_TOKEN not persisted", async () => {
|
|
552
|
+
const patchCalls: PatchCall[] = [];
|
|
553
|
+
const updateFieldsCalls: UpdateFieldsCall[] = [];
|
|
554
|
+
const failingAuthTestClient: AdminUISlackClient = {
|
|
555
|
+
createAppManifest: async () => {
|
|
556
|
+
throw new Error("unused in this test");
|
|
557
|
+
},
|
|
558
|
+
updateAppManifest: async () => {},
|
|
559
|
+
exchangeOAuthCode: async () => ({
|
|
560
|
+
botToken: "xoxb-test-cassette-bot-token",
|
|
561
|
+
}),
|
|
562
|
+
authTest: async () => {
|
|
563
|
+
throw new Error("token_revoked");
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
const service = makeService({
|
|
567
|
+
slackClient: failingAuthTestClient,
|
|
568
|
+
patchCalls,
|
|
569
|
+
updateFieldsCalls,
|
|
570
|
+
});
|
|
571
|
+
const token = await makeProvisionStateToken();
|
|
572
|
+
|
|
573
|
+
const result = await service.completeConnect(
|
|
574
|
+
token,
|
|
575
|
+
"valid-oauth-code",
|
|
576
|
+
CONNECT_SLACK_CALLBACK,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(result.outcome).toBe("exchange_failed");
|
|
580
|
+
if (result.outcome !== "exchange_failed") throw new Error("wrong outcome");
|
|
581
|
+
expect(result.error).toContain("token_revoked");
|
|
582
|
+
expect(patchCalls.length).toBe(0);
|
|
583
|
+
expect(updateFieldsCalls.length).toBe(0);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
describe("SlackProvisioningService.saveAppToken", () => {
|
|
588
|
+
it("valid xapp- token → stores SLACK_APP_TOKEN and reconciles system crons", async () => {
|
|
589
|
+
const patchCalls: PatchCall[] = [];
|
|
590
|
+
const reconcileCalls: string[] = [];
|
|
591
|
+
const service = makeService({ patchCalls, reconcileCalls });
|
|
592
|
+
|
|
593
|
+
const result = await service.saveAppToken(
|
|
594
|
+
AGENT_ID,
|
|
595
|
+
"xapp-1-TEST-fake-socket-token",
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
expect(result.ok).toBe(true);
|
|
599
|
+
if (!result.ok) throw new Error("expected ok result");
|
|
600
|
+
expect(result.agentId).toBe(AGENT_ID);
|
|
601
|
+
|
|
602
|
+
expect(patchCalls.length).toBe(1);
|
|
603
|
+
expect(patchCalls[0].env.SLACK_APP_TOKEN).toBe(
|
|
604
|
+
"xapp-1-TEST-fake-socket-token",
|
|
605
|
+
);
|
|
606
|
+
expect(patchCalls[0].secretKeys).toContain("SLACK_APP_TOKEN");
|
|
607
|
+
expect(reconcileCalls).toContain(AGENT_ID);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("missing agentId → ok: false, no env write, no cron reconcile", async () => {
|
|
611
|
+
const patchCalls: PatchCall[] = [];
|
|
612
|
+
const reconcileCalls: string[] = [];
|
|
613
|
+
const service = makeService({ patchCalls, reconcileCalls });
|
|
614
|
+
|
|
615
|
+
const result = await service.saveAppToken(
|
|
616
|
+
undefined,
|
|
617
|
+
"xapp-1-TEST-fake-socket-token",
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
expect(result.ok).toBe(false);
|
|
621
|
+
expect(patchCalls.length).toBe(0);
|
|
622
|
+
expect(reconcileCalls.length).toBe(0);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("xappToken missing xapp- prefix → ok: false, no env write", async () => {
|
|
626
|
+
const patchCalls: PatchCall[] = [];
|
|
627
|
+
const reconcileCalls: string[] = [];
|
|
628
|
+
const service = makeService({ patchCalls, reconcileCalls });
|
|
629
|
+
|
|
630
|
+
const result = await service.saveAppToken(AGENT_ID, "not-a-valid-token");
|
|
631
|
+
|
|
632
|
+
expect(result.ok).toBe(false);
|
|
633
|
+
if (result.ok) throw new Error("expected error result");
|
|
634
|
+
expect(result.error).toContain("xapp-");
|
|
635
|
+
expect(patchCalls.length).toBe(0);
|
|
636
|
+
});
|
|
637
|
+
});
|