@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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/startup-policy-reconcile.unit.test.ts
|
|
3
|
+
* Unit tests for runStartupPolicyReconcile (ACP-1.3).
|
|
4
|
+
*
|
|
5
|
+
* agentService, provisioner and logger are all plain injected doubles — no
|
|
6
|
+
* mock.module(), no global overrides, per the repo's test-isolation rule
|
|
7
|
+
* (CLAUDE.md).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import type { AgentProvisioner, ReconcileResult } from "./agent-provisioner.ts";
|
|
12
|
+
import type { AgentSummary } from "./agents.ts";
|
|
13
|
+
import {
|
|
14
|
+
managedReconcileTargets,
|
|
15
|
+
runStartupPolicyReconcile,
|
|
16
|
+
} from "./startup-policy-reconcile.ts";
|
|
17
|
+
|
|
18
|
+
// ─── Doubles ────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
type ReconcileCall = {
|
|
21
|
+
agents: Parameters<AgentProvisioner["reconcile"]>[0];
|
|
22
|
+
opts: Parameters<AgentProvisioner["reconcile"]>[1];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const EMPTY_RESULT: ReconcileResult = {
|
|
26
|
+
recreated: [],
|
|
27
|
+
migrated: [],
|
|
28
|
+
noOwner: [],
|
|
29
|
+
dryRun: false,
|
|
30
|
+
orphans: [],
|
|
31
|
+
failed: [],
|
|
32
|
+
updated: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function fakeProvisioner(opts: {
|
|
36
|
+
canProvision: boolean;
|
|
37
|
+
reconcile?: () => Promise<ReconcileResult>;
|
|
38
|
+
}) {
|
|
39
|
+
const calls: ReconcileCall[] = [];
|
|
40
|
+
const provisioner = {
|
|
41
|
+
canProvision: opts.canProvision,
|
|
42
|
+
async reconcile(
|
|
43
|
+
agents: ReconcileCall["agents"],
|
|
44
|
+
reconcileOpts: ReconcileCall["opts"],
|
|
45
|
+
) {
|
|
46
|
+
calls.push({ agents, opts: reconcileOpts });
|
|
47
|
+
return opts.reconcile ? opts.reconcile() : EMPTY_RESULT;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return { provisioner, calls };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fakeAgentService(
|
|
54
|
+
agents: AgentSummary[] | (() => Promise<AgentSummary[]>),
|
|
55
|
+
) {
|
|
56
|
+
let listCalls = 0;
|
|
57
|
+
return {
|
|
58
|
+
agentService: {
|
|
59
|
+
async list() {
|
|
60
|
+
listCalls++;
|
|
61
|
+
return typeof agents === "function" ? agents() : agents;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
listCalls: () => listCalls,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function recordingLogger() {
|
|
69
|
+
const lines: { level: "log" | "error"; args: unknown[] }[] = [];
|
|
70
|
+
return {
|
|
71
|
+
logger: {
|
|
72
|
+
log: (...args: unknown[]) => lines.push({ level: "log", args }),
|
|
73
|
+
error: (...args: unknown[]) => lines.push({ level: "error", args }),
|
|
74
|
+
},
|
|
75
|
+
lines,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function agent(overrides: Partial<AgentSummary> & { id: string }) {
|
|
80
|
+
return {
|
|
81
|
+
name: `${overrides.id}-slug`,
|
|
82
|
+
selfHosted: false,
|
|
83
|
+
typeName: "dev",
|
|
84
|
+
accountId: null,
|
|
85
|
+
...overrides,
|
|
86
|
+
} satisfies AgentSummary;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── managedReconcileTargets ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe("managedReconcileTargets", () => {
|
|
92
|
+
it("drops self-hosted agents and maps name to slug", () => {
|
|
93
|
+
expect(
|
|
94
|
+
managedReconcileTargets([
|
|
95
|
+
agent({ id: "a1", name: "alpha", accountId: "acct-1" }),
|
|
96
|
+
agent({ id: "a2", selfHosted: true }),
|
|
97
|
+
agent({ id: "a3", name: "gamma" }),
|
|
98
|
+
]),
|
|
99
|
+
).toEqual([
|
|
100
|
+
{ id: "a1", slug: "alpha", accountId: "acct-1" },
|
|
101
|
+
{ id: "a3", slug: "gamma", accountId: null },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("managedReconcileTargets identity fields", () => {
|
|
107
|
+
it("carries the owner and per-agent identity fields of each agent", () => {
|
|
108
|
+
expect(
|
|
109
|
+
managedReconcileTargets([
|
|
110
|
+
agent({
|
|
111
|
+
id: "a1",
|
|
112
|
+
name: "alpha",
|
|
113
|
+
ownerEmail: "jane.doe@example.com",
|
|
114
|
+
anthropicIdentityName: "jane-doe-a1",
|
|
115
|
+
anthropicServiceAccountId: "svac_1",
|
|
116
|
+
anthropicFederationRuleId: "fdrl_1",
|
|
117
|
+
k8sServiceAccountName: "a1",
|
|
118
|
+
}),
|
|
119
|
+
agent({ id: "a2", name: "beta", ownerEmail: null }),
|
|
120
|
+
]),
|
|
121
|
+
).toEqual([
|
|
122
|
+
{
|
|
123
|
+
id: "a1",
|
|
124
|
+
slug: "alpha",
|
|
125
|
+
accountId: null,
|
|
126
|
+
ownerEmail: "jane.doe@example.com",
|
|
127
|
+
anthropicIdentityName: "jane-doe-a1",
|
|
128
|
+
anthropicServiceAccountId: "svac_1",
|
|
129
|
+
anthropicFederationRuleId: "fdrl_1",
|
|
130
|
+
k8sServiceAccountName: "a1",
|
|
131
|
+
},
|
|
132
|
+
{ id: "a2", slug: "beta", accountId: null, ownerEmail: null },
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── runStartupPolicyReconcile ──────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("runStartupPolicyReconcile", () => {
|
|
140
|
+
it("reconciles non-self-hosted agents with policyOnly true", async () => {
|
|
141
|
+
const { agentService } = fakeAgentService([
|
|
142
|
+
agent({ id: "a1", name: "alpha" }),
|
|
143
|
+
agent({ id: "a2", name: "beta", selfHosted: true }),
|
|
144
|
+
agent({ id: "a3", name: "gamma", accountId: "acct-3" }),
|
|
145
|
+
]);
|
|
146
|
+
const { provisioner, calls } = fakeProvisioner({ canProvision: true });
|
|
147
|
+
const { logger } = recordingLogger();
|
|
148
|
+
|
|
149
|
+
await runStartupPolicyReconcile({ agentService, provisioner, logger });
|
|
150
|
+
|
|
151
|
+
expect(calls).toEqual([
|
|
152
|
+
{
|
|
153
|
+
agents: [
|
|
154
|
+
{ id: "a1", slug: "alpha", accountId: null },
|
|
155
|
+
{ id: "a3", slug: "gamma", accountId: "acct-3" },
|
|
156
|
+
],
|
|
157
|
+
opts: { policyOnly: true },
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("does nothing when the provisioner cannot provision", async () => {
|
|
163
|
+
const { agentService, listCalls } = fakeAgentService([agent({ id: "a1" })]);
|
|
164
|
+
const { provisioner, calls } = fakeProvisioner({ canProvision: false });
|
|
165
|
+
const { logger, lines } = recordingLogger();
|
|
166
|
+
|
|
167
|
+
await runStartupPolicyReconcile({ agentService, provisioner, logger });
|
|
168
|
+
|
|
169
|
+
expect(calls).toHaveLength(0);
|
|
170
|
+
expect(listCalls()).toBe(0);
|
|
171
|
+
expect(lines).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("logs a provisioner error instead of throwing", async () => {
|
|
175
|
+
const { agentService } = fakeAgentService([agent({ id: "a1" })]);
|
|
176
|
+
const boom = new Error("kube api unreachable");
|
|
177
|
+
const { provisioner } = fakeProvisioner({
|
|
178
|
+
canProvision: true,
|
|
179
|
+
reconcile: () => Promise.reject(boom),
|
|
180
|
+
});
|
|
181
|
+
const { logger, lines } = recordingLogger();
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
runStartupPolicyReconcile({ agentService, provisioner, logger }),
|
|
185
|
+
).resolves.toBeUndefined();
|
|
186
|
+
|
|
187
|
+
const errors = lines.filter((l) => l.level === "error");
|
|
188
|
+
expect(errors).toHaveLength(1);
|
|
189
|
+
expect(errors[0]?.args).toContain(boom);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("logs an agent listing error instead of throwing", async () => {
|
|
193
|
+
const boom = new Error("db down");
|
|
194
|
+
const { agentService } = fakeAgentService(() => Promise.reject(boom));
|
|
195
|
+
const { provisioner, calls } = fakeProvisioner({ canProvision: true });
|
|
196
|
+
const { logger, lines } = recordingLogger();
|
|
197
|
+
|
|
198
|
+
await expect(
|
|
199
|
+
runStartupPolicyReconcile({ agentService, provisioner, logger }),
|
|
200
|
+
).resolves.toBeUndefined();
|
|
201
|
+
|
|
202
|
+
expect(calls).toHaveLength(0);
|
|
203
|
+
const errors = lines.filter((l) => l.level === "error");
|
|
204
|
+
expect(errors).toHaveLength(1);
|
|
205
|
+
expect(errors[0]?.args).toContain(boom);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("logs the reconcile outcome on success", async () => {
|
|
209
|
+
const { agentService } = fakeAgentService([agent({ id: "a1" })]);
|
|
210
|
+
const { provisioner } = fakeProvisioner({
|
|
211
|
+
canProvision: true,
|
|
212
|
+
reconcile: async () => ({
|
|
213
|
+
...EMPTY_RESULT,
|
|
214
|
+
recreated: ["a1"],
|
|
215
|
+
updated: ["a2", "a3"],
|
|
216
|
+
orphans: ["x9"],
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
const { logger, lines } = recordingLogger();
|
|
220
|
+
|
|
221
|
+
await runStartupPolicyReconcile({ agentService, provisioner, logger });
|
|
222
|
+
|
|
223
|
+
expect(lines.filter((l) => l.level === "error")).toHaveLength(0);
|
|
224
|
+
const text = lines.map((l) => l.args.join(" ")).join("\n");
|
|
225
|
+
expect(text).toContain("updated=2");
|
|
226
|
+
expect(text).toContain("recreated=1");
|
|
227
|
+
expect(text).toContain("orphans=1");
|
|
228
|
+
expect(text).toContain("noOwner=0");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("logs per-agent failures from the reconcile result as errors", async () => {
|
|
232
|
+
const { agentService } = fakeAgentService([agent({ id: "a1" })]);
|
|
233
|
+
const { provisioner } = fakeProvisioner({
|
|
234
|
+
canProvision: true,
|
|
235
|
+
reconcile: async () => ({
|
|
236
|
+
...EMPTY_RESULT,
|
|
237
|
+
failed: [{ agentId: "a1", error: "patch rejected" }],
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
const { logger, lines } = recordingLogger();
|
|
241
|
+
|
|
242
|
+
await runStartupPolicyReconcile({ agentService, provisioner, logger });
|
|
243
|
+
|
|
244
|
+
const errors = lines
|
|
245
|
+
.filter((l) => l.level === "error")
|
|
246
|
+
.map((l) => l.args.join(" "));
|
|
247
|
+
expect(errors).toHaveLength(1);
|
|
248
|
+
expect(errors[0]).toContain("a1");
|
|
249
|
+
expect(errors[0]).toContain("patch rejected");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Admin's own copy of the brand design tokens it uses (subset of brand/tokens.json). Admin must not import outside admin/, so this is duplicated; brand/admin-theme-tokens.unit.test.ts fails if it drifts from brand/tokens.json.",
|
|
3
|
+
"color": {
|
|
4
|
+
"bg": { "base": "#080E1E" },
|
|
5
|
+
"brand": { "default": "#34C77B" },
|
|
6
|
+
"text": {
|
|
7
|
+
"heading": "#FFFFFF",
|
|
8
|
+
"body": "rgba(255,255,255,0.80)",
|
|
9
|
+
"editorial": "rgba(255,255,255,0.50)"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/token-crypto.ts
|
|
3
|
+
* Token encryption/decryption interface and factory for agent credential storage.
|
|
4
|
+
*
|
|
5
|
+
* Centralises all token crypto so every storage path encrypts and decrypts
|
|
6
|
+
* automatically — callers work with plain text.
|
|
7
|
+
*
|
|
8
|
+
* Key: SHIPWRIGHT_ENCRYPTION_KEY — 64-char hex string (32 bytes for AES-256-GCM).
|
|
9
|
+
* If unset: identity functions (plain text stored, no encryption).
|
|
10
|
+
* If set but malformed: makeTokenCrypto() throws at construction — fail fast at
|
|
11
|
+
* boot with an actionable message, instead of an opaque runtime 500 on the
|
|
12
|
+
* first encrypted write.
|
|
13
|
+
*
|
|
14
|
+
* Legacy plain-text values: decrypt() catches parse errors and returns the stored
|
|
15
|
+
* value as-is, so existing rows continue to work after the key is added.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { decrypt, encrypt, isValidKeyHex } from "./crypto.ts";
|
|
19
|
+
|
|
20
|
+
export interface TokenCrypto {
|
|
21
|
+
encrypt(token: string): string;
|
|
22
|
+
decrypt(token: string): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Identity implementation — no encryption. Used as default in tests and when key is unset. */
|
|
26
|
+
export const identityCrypto: TokenCrypto = {
|
|
27
|
+
encrypt: (s) => s,
|
|
28
|
+
decrypt: (s) => s,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a TokenCrypto backed by AES-256-GCM using SHIPWRIGHT_ENCRYPTION_KEY.
|
|
33
|
+
* Falls back to identity (plain text) if the env var is not set.
|
|
34
|
+
*/
|
|
35
|
+
export function makeTokenCrypto(
|
|
36
|
+
key = process.env.SHIPWRIGHT_ENCRYPTION_KEY,
|
|
37
|
+
): TokenCrypto {
|
|
38
|
+
if (!key) {
|
|
39
|
+
console.warn(
|
|
40
|
+
"[shipwright admin] SHIPWRIGHT_ENCRYPTION_KEY not set — tokens stored in plain text",
|
|
41
|
+
);
|
|
42
|
+
return identityCrypto;
|
|
43
|
+
}
|
|
44
|
+
if (!isValidKeyHex(key)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`SHIPWRIGHT_ENCRYPTION_KEY must be a 64-character hex string (32 bytes for AES-256-GCM), but got ${key.length} character(s). Generate one with: openssl rand -hex 32`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
encrypt: (token) => encrypt(token, key),
|
|
51
|
+
decrypt: (encrypted) => {
|
|
52
|
+
try {
|
|
53
|
+
return decrypt(encrypted, key);
|
|
54
|
+
} catch {
|
|
55
|
+
// Legacy plain-text value — return as-is so existing rows keep working.
|
|
56
|
+
return encrypted;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/src/token-crypto.unit.test.ts
|
|
3
|
+
* Unit tests for the crypto round-trip (AES-256-GCM).
|
|
4
|
+
* Pure logic — no I/O, no DB, no network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
8
|
+
import { decrypt, encrypt, isValidKeyHex } from "./crypto.ts";
|
|
9
|
+
import { identityCrypto, makeTokenCrypto } from "./token-crypto.ts";
|
|
10
|
+
|
|
11
|
+
// ─── Test key ─────────────────────────────────────────────────────────────────
|
|
12
|
+
// A known 64-char hex key (32 bytes) for deterministic tests.
|
|
13
|
+
const TEST_KEY =
|
|
14
|
+
"0000000000000000000000000000000000000000000000000000000000000001";
|
|
15
|
+
|
|
16
|
+
// ─── crypto.ts: encrypt / decrypt ─────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("encrypt / decrypt (AES-256-GCM)", () => {
|
|
19
|
+
it("round-trips a plain ASCII string", () => {
|
|
20
|
+
const plaintext = "hello world";
|
|
21
|
+
const encrypted = encrypt(plaintext, TEST_KEY);
|
|
22
|
+
expect(decrypt(encrypted, TEST_KEY)).toBe(plaintext);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("round-trips an empty string", () => {
|
|
26
|
+
const plaintext = "";
|
|
27
|
+
const encrypted = encrypt(plaintext, TEST_KEY);
|
|
28
|
+
expect(decrypt(encrypted, TEST_KEY)).toBe(plaintext);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("round-trips a unicode string", () => {
|
|
32
|
+
const plaintext = "こんにちは世界 🌍 — café résumé naïve";
|
|
33
|
+
const encrypted = encrypt(plaintext, TEST_KEY);
|
|
34
|
+
expect(decrypt(encrypted, TEST_KEY)).toBe(plaintext);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("round-trips a 4096-byte value", () => {
|
|
38
|
+
const plaintext = "x".repeat(4096);
|
|
39
|
+
const encrypted = encrypt(plaintext, TEST_KEY);
|
|
40
|
+
expect(decrypt(encrypted, TEST_KEY)).toBe(plaintext);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("produces a different ciphertext on each call (random IV)", () => {
|
|
44
|
+
const plaintext = "same input";
|
|
45
|
+
const enc1 = encrypt(plaintext, TEST_KEY);
|
|
46
|
+
const enc2 = encrypt(plaintext, TEST_KEY);
|
|
47
|
+
expect(enc1).not.toBe(enc2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("encrypted format is iv:ciphertext:authTag (3 colon-separated parts)", () => {
|
|
51
|
+
const encrypted = encrypt("test", TEST_KEY);
|
|
52
|
+
const parts = encrypted.split(":");
|
|
53
|
+
expect(parts.length).toBe(3);
|
|
54
|
+
// IV is 12 bytes = 24 hex chars
|
|
55
|
+
expect(parts[0]).toHaveLength(24);
|
|
56
|
+
// Auth tag is 16 bytes = 32 hex chars
|
|
57
|
+
expect(parts[2]).toHaveLength(32);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("throws on invalid encrypted format (wrong part count)", () => {
|
|
61
|
+
expect(() => decrypt("notvalid", TEST_KEY)).toThrow(
|
|
62
|
+
"Invalid encrypted format",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("throws when auth tag is wrong (tampered ciphertext)", () => {
|
|
67
|
+
const encrypted = encrypt("secret", TEST_KEY);
|
|
68
|
+
const parts = encrypted.split(":");
|
|
69
|
+
// Flip one char in the ciphertext
|
|
70
|
+
parts[1] = parts[1]
|
|
71
|
+
? parts[1].slice(0, -2) + (parts[1].slice(-2) === "ff" ? "00" : "ff")
|
|
72
|
+
: "00";
|
|
73
|
+
const tampered = parts.join(":");
|
|
74
|
+
expect(() => decrypt(tampered, TEST_KEY)).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ─── isValidKeyHex ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("isValidKeyHex", () => {
|
|
81
|
+
it("accepts a 64-char lowercase hex string", () => {
|
|
82
|
+
expect(isValidKeyHex(TEST_KEY)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("accepts uppercase hex", () => {
|
|
86
|
+
expect(isValidKeyHex("A".repeat(64))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects a 32-char value (the chart's old randAlphaNum output length)", () => {
|
|
90
|
+
expect(isValidKeyHex("a".repeat(32))).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects a 64-char string with non-hex characters", () => {
|
|
94
|
+
expect(isValidKeyHex("z".repeat(64))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("rejects an empty string", () => {
|
|
98
|
+
expect(isValidKeyHex("")).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─── makeTokenCrypto — key validation (via injected key, no env mutation) ───────
|
|
103
|
+
|
|
104
|
+
describe("makeTokenCrypto — key validation", () => {
|
|
105
|
+
it("returns identityCrypto when the key is undefined", () => {
|
|
106
|
+
const crypto = makeTokenCrypto(undefined);
|
|
107
|
+
expect(crypto.encrypt("t")).toBe("t");
|
|
108
|
+
expect(crypto.decrypt("t")).toBe("t");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns identityCrypto when the key is an empty string", () => {
|
|
112
|
+
expect(makeTokenCrypto("").encrypt("t")).toBe("t");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("throws an actionable error when the key is 32 chars (the old chart bug)", () => {
|
|
116
|
+
expect(() => makeTokenCrypto("a".repeat(32))).toThrow(
|
|
117
|
+
/64-character hex string/,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws when the key contains non-hex characters", () => {
|
|
122
|
+
expect(() => makeTokenCrypto("z".repeat(64))).toThrow(/64-character hex/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("constructs a working crypto for a valid 64-char hex key", () => {
|
|
126
|
+
const crypto = makeTokenCrypto(TEST_KEY);
|
|
127
|
+
const enc = crypto.encrypt("secret");
|
|
128
|
+
expect(enc).not.toBe("secret");
|
|
129
|
+
expect(crypto.decrypt(enc)).toBe("secret");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── makeTokenCrypto ───────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
describe("makeTokenCrypto", () => {
|
|
136
|
+
const originalKey = process.env.SHIPWRIGHT_ENCRYPTION_KEY;
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
if (originalKey === undefined) {
|
|
140
|
+
delete process.env.SHIPWRIGHT_ENCRYPTION_KEY;
|
|
141
|
+
} else {
|
|
142
|
+
process.env.SHIPWRIGHT_ENCRYPTION_KEY = originalKey;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns identityCrypto when SHIPWRIGHT_ENCRYPTION_KEY is not set", () => {
|
|
147
|
+
delete process.env.SHIPWRIGHT_ENCRYPTION_KEY;
|
|
148
|
+
const crypto = makeTokenCrypto();
|
|
149
|
+
const token = "my-secret-token";
|
|
150
|
+
expect(crypto.encrypt(token)).toBe(token);
|
|
151
|
+
expect(crypto.decrypt(token)).toBe(token);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("encrypts and decrypts when key is set", () => {
|
|
155
|
+
process.env.SHIPWRIGHT_ENCRYPTION_KEY = TEST_KEY;
|
|
156
|
+
const crypto = makeTokenCrypto();
|
|
157
|
+
const token = "my-secret-token";
|
|
158
|
+
const encrypted = crypto.encrypt(token);
|
|
159
|
+
expect(encrypted).not.toBe(token);
|
|
160
|
+
expect(crypto.decrypt(encrypted)).toBe(token);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("decrypt falls back to returning plain text for legacy unencrypted tokens", () => {
|
|
164
|
+
process.env.SHIPWRIGHT_ENCRYPTION_KEY = TEST_KEY;
|
|
165
|
+
const crypto = makeTokenCrypto();
|
|
166
|
+
const legacyToken = "plain-text-stored-before-encryption";
|
|
167
|
+
// decrypt should not throw — returns the value as-is
|
|
168
|
+
expect(crypto.decrypt(legacyToken)).toBe(legacyToken);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ─── identityCrypto ────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe("identityCrypto", () => {
|
|
175
|
+
it("encrypt returns input unchanged", () => {
|
|
176
|
+
expect(identityCrypto.encrypt("hello")).toBe("hello");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("decrypt returns input unchanged", () => {
|
|
180
|
+
expect(identityCrypto.decrypt("hello")).toBe("hello");
|
|
181
|
+
});
|
|
182
|
+
});
|