create-ailk 0.5.0 → 0.6.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/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -33,6 +33,7 @@ jest.mock("@working-theory/observability", () => ({
|
|
|
33
33
|
import { prisma } from "../../lib/prisma.js";
|
|
34
34
|
import {
|
|
35
35
|
fulfillDeliverable,
|
|
36
|
+
listSendlessGrants,
|
|
36
37
|
type FulfillDeliverableInput,
|
|
37
38
|
} from "../deliverable-fulfillment";
|
|
38
39
|
|
|
@@ -45,6 +46,17 @@ const SESSION_ID = "flsess_00000000-0000-7000-8000-0000000000c1";
|
|
|
45
46
|
const LEAD_ID = "lead_00000000-0000-7000-8000-0000000000e1";
|
|
46
47
|
const GRANT_ID = "dgrant_00000000-0000-7000-8000-0000000000g1";
|
|
47
48
|
|
|
49
|
+
// #6257 — the seeded sentinel workspace the `"default"` alias resolves to
|
|
50
|
+
// (`@working-theory/database/tenancy`'s DEFAULT_WORKSPACE_ID), with a
|
|
51
|
+
// sentinel-shaped organizationId so `resolveSentinelWorkspaceRow`'s own
|
|
52
|
+
// `isSentinel` check passes. Used by ONE test — the `listSendlessGrants`
|
|
53
|
+
// resolution test, the only test in this file that exercises an alias at
|
|
54
|
+
// all — to prove that read scopes by the id it RESOLVED rather than by the
|
|
55
|
+
// alias string the CLI was given. The other fence tests below run no
|
|
56
|
+
// resolution: `fulfillDeliverable` is handed an already-resolved id.
|
|
57
|
+
const SENTINEL_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
58
|
+
const SENTINEL_ORGANIZATION_ID = "org_00000000-0000-7000-8000-000000000001";
|
|
59
|
+
|
|
48
60
|
function deliverablesBlock() {
|
|
49
61
|
return {
|
|
50
62
|
assets: [
|
|
@@ -122,7 +134,12 @@ describe("fulfillDeliverable", () => {
|
|
|
122
134
|
|
|
123
135
|
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
|
|
124
136
|
expect.objectContaining({
|
|
125
|
-
where: {
|
|
137
|
+
where: {
|
|
138
|
+
siteId_key: { siteId: SITE_ID, key: "guide-small" },
|
|
139
|
+
// #6257 — the legacy store's read is fenced by the same resolved
|
|
140
|
+
// workspace id the grant is minted under.
|
|
141
|
+
workspaceId: SITE_ID,
|
|
142
|
+
},
|
|
126
143
|
}),
|
|
127
144
|
);
|
|
128
145
|
});
|
|
@@ -138,7 +155,10 @@ describe("fulfillDeliverable", () => {
|
|
|
138
155
|
|
|
139
156
|
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
|
|
140
157
|
expect.objectContaining({
|
|
141
|
-
where: {
|
|
158
|
+
where: {
|
|
159
|
+
siteId_key: { siteId: SITE_ID, key: "guide-large" },
|
|
160
|
+
workspaceId: SITE_ID,
|
|
161
|
+
},
|
|
142
162
|
}),
|
|
143
163
|
);
|
|
144
164
|
});
|
|
@@ -148,7 +168,10 @@ describe("fulfillDeliverable", () => {
|
|
|
148
168
|
|
|
149
169
|
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
|
|
150
170
|
expect.objectContaining({
|
|
151
|
-
where: {
|
|
171
|
+
where: {
|
|
172
|
+
siteId_key: { siteId: SITE_ID, key: "guide-default" },
|
|
173
|
+
workspaceId: SITE_ID,
|
|
174
|
+
},
|
|
152
175
|
}),
|
|
153
176
|
);
|
|
154
177
|
});
|
|
@@ -158,7 +181,10 @@ describe("fulfillDeliverable", () => {
|
|
|
158
181
|
|
|
159
182
|
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
|
|
160
183
|
expect.objectContaining({
|
|
161
|
-
where: {
|
|
184
|
+
where: {
|
|
185
|
+
siteId_key: { siteId: SITE_ID, key: "guide-default" },
|
|
186
|
+
workspaceId: SITE_ID,
|
|
187
|
+
},
|
|
162
188
|
}),
|
|
163
189
|
);
|
|
164
190
|
});
|
|
@@ -178,6 +204,9 @@ describe("fulfillDeliverable", () => {
|
|
|
178
204
|
);
|
|
179
205
|
|
|
180
206
|
expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
|
|
207
|
+
// #6310 FR8: a no_entry resolution exits before EITHER store is read.
|
|
208
|
+
expect(prisma.flowDefinition.findUnique).not.toHaveBeenCalled();
|
|
209
|
+
expect(prisma.asset.findFirst).not.toHaveBeenCalled();
|
|
181
210
|
expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
|
|
182
211
|
expect(mockSendDeliverableEmail).not.toHaveBeenCalled();
|
|
183
212
|
// AC4: no_entry is a pre-mint outcome — no grant exists to stamp.
|
|
@@ -234,7 +263,13 @@ describe("fulfillDeliverable", () => {
|
|
|
234
263
|
|
|
235
264
|
expect(prisma.playbookInstance.findFirst).toHaveBeenCalledWith(
|
|
236
265
|
expect.objectContaining({
|
|
237
|
-
where: {
|
|
266
|
+
where: {
|
|
267
|
+
siteId: SITE_ID,
|
|
268
|
+
// #6257 — the playbook existence check is fenced too.
|
|
269
|
+
workspaceId: SITE_ID,
|
|
270
|
+
leadId: LEAD_ID,
|
|
271
|
+
experimentKey: "targeting-maturity",
|
|
272
|
+
},
|
|
238
273
|
}),
|
|
239
274
|
);
|
|
240
275
|
expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
|
|
@@ -296,6 +331,166 @@ describe("fulfillDeliverable", () => {
|
|
|
296
331
|
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
|
|
297
332
|
});
|
|
298
333
|
|
|
334
|
+
it("a playbook: deliverable issues zero Asset or FlowDefinition reads (#6310 FR8)", async () => {
|
|
335
|
+
(prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({
|
|
336
|
+
id: "pbk_00000000-0000-7000-8000-0000000000p1",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
await fulfillDeliverable(
|
|
340
|
+
baseInput({ segment: undefined, configSnapshot: playbookConfigSnapshot() }),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
expect(prisma.flowDefinition.findUnique).not.toHaveBeenCalled();
|
|
344
|
+
expect(prisma.asset.findFirst).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ── #6310 D1/D2/D4/D5/D15 — Asset-first resolution ───────────────────────
|
|
348
|
+
|
|
349
|
+
const WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
350
|
+
const OTHER_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000002";
|
|
351
|
+
const NEW_ASSET_ID = "asset_00000000-0000-7000-8000-0000000000b1";
|
|
352
|
+
|
|
353
|
+
it("resolves against the workspace Asset store first, and mints the grant with the asset_ id AND workspaceId (D1/D2/D15)", async () => {
|
|
354
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
355
|
+
workspaceId: WORKSPACE_ID,
|
|
356
|
+
});
|
|
357
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID });
|
|
358
|
+
|
|
359
|
+
await fulfillDeliverable(baseInput());
|
|
360
|
+
|
|
361
|
+
expect(prisma.flowDefinition.findUnique).toHaveBeenCalledWith(
|
|
362
|
+
expect.objectContaining({
|
|
363
|
+
where: { siteId_slug: { siteId: SITE_ID, slug: "get-a-quote" } },
|
|
364
|
+
select: { workspaceId: true },
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
expect(prisma.asset.findFirst).toHaveBeenCalledWith({
|
|
368
|
+
where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
|
|
369
|
+
select: { id: true },
|
|
370
|
+
});
|
|
371
|
+
expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
|
|
372
|
+
expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
|
|
373
|
+
expect.objectContaining({
|
|
374
|
+
data: expect.objectContaining({
|
|
375
|
+
siteId: SITE_ID,
|
|
376
|
+
assetId: NEW_ASSET_ID,
|
|
377
|
+
workspaceId: WORKSPACE_ID,
|
|
378
|
+
leadId: LEAD_ID,
|
|
379
|
+
sessionId: SESSION_ID,
|
|
380
|
+
}),
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("falls back to the legacy DeliverableAsset store on an Asset miss, and mints a grant with NO workspaceId (D2/D3)", async () => {
|
|
386
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
387
|
+
workspaceId: WORKSPACE_ID,
|
|
388
|
+
});
|
|
389
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
|
|
390
|
+
|
|
391
|
+
await fulfillDeliverable(baseInput());
|
|
392
|
+
|
|
393
|
+
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalledWith(
|
|
394
|
+
expect.objectContaining({
|
|
395
|
+
where: {
|
|
396
|
+
siteId_key: { siteId: SITE_ID, key: "guide-small" },
|
|
397
|
+
// #6257 — the legacy store's read is fenced by the same resolved
|
|
398
|
+
// workspace id the grant is minted under.
|
|
399
|
+
workspaceId: SITE_ID,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
const [[createArgs]] = (prisma.deliverableGrant.create as jest.Mock).mock
|
|
404
|
+
.calls as [[{ data: Record<string, unknown> }]];
|
|
405
|
+
expect(createArgs.data["assetId"]).toBe(
|
|
406
|
+
"dasset_00000000-0000-7000-8000-0000000000a1",
|
|
407
|
+
);
|
|
408
|
+
// #6374 D4 / AC-8 — this mint site now ALWAYS stamps `workspaceId`,
|
|
409
|
+
// including on the legacy `dasset_` arm this test exercises, and the
|
|
410
|
+
// value is the grant's own `siteId`. NULL remains the sentinel for the
|
|
411
|
+
// rows that already EXIST (the migration touches none of them); it is no
|
|
412
|
+
// longer the sentinel for a row this code mints, because a mint always
|
|
413
|
+
// knows its workspace. The download route is unaffected: it branches on
|
|
414
|
+
// the `assetId` PREFIX and consults `workspaceId` only on the `asset_`
|
|
415
|
+
// arm.
|
|
416
|
+
expect(createArgs.data["workspaceId"]).toBe(createArgs.data["siteId"]);
|
|
417
|
+
expect(createArgs.data["workspaceId"]).toBe(SITE_ID);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("prefers the Asset match over an existing legacy DeliverableAsset row at the SAME key (D2 order)", async () => {
|
|
421
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
422
|
+
workspaceId: WORKSPACE_ID,
|
|
423
|
+
});
|
|
424
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue({ id: NEW_ASSET_ID });
|
|
425
|
+
// beforeEach's default deliverableAsset.findUnique mock already resolves
|
|
426
|
+
// a row at this key — it must never even be consulted.
|
|
427
|
+
|
|
428
|
+
await fulfillDeliverable(baseInput());
|
|
429
|
+
|
|
430
|
+
expect(prisma.deliverableAsset.findUnique).not.toHaveBeenCalled();
|
|
431
|
+
expect(prisma.deliverableGrant.create).toHaveBeenCalledWith(
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
data: expect.objectContaining({ assetId: NEW_ASSET_ID }),
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("skips the Asset lookup entirely when the definition's workspaceId is null — never guesses one (D5)", async () => {
|
|
439
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
440
|
+
workspaceId: null,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await fulfillDeliverable(baseInput());
|
|
444
|
+
|
|
445
|
+
expect(prisma.asset.findFirst).not.toHaveBeenCalled();
|
|
446
|
+
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("skips the Asset lookup entirely when no FlowDefinition row resolves (D5)", async () => {
|
|
450
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue(null);
|
|
451
|
+
|
|
452
|
+
await fulfillDeliverable(baseInput());
|
|
453
|
+
|
|
454
|
+
expect(prisma.asset.findFirst).not.toHaveBeenCalled();
|
|
455
|
+
expect(prisma.deliverableAsset.findUnique).toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("mints no grant and logs asset_missing when NEITHER store resolves the key", async () => {
|
|
459
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
460
|
+
workspaceId: WORKSPACE_ID,
|
|
461
|
+
});
|
|
462
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
|
|
463
|
+
(prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
|
|
464
|
+
|
|
465
|
+
await fulfillDeliverable(baseInput());
|
|
466
|
+
|
|
467
|
+
expect(prisma.deliverableGrant.create).not.toHaveBeenCalled();
|
|
468
|
+
expect(mockLoggerInfo).toHaveBeenCalledWith(
|
|
469
|
+
expect.objectContaining({ outcome: "asset_missing" }),
|
|
470
|
+
expect.any(String),
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("never reaches a key that exists only in ANOTHER workspace (NFR3) — the lookup is scoped to this flow's own workspaceId alone", async () => {
|
|
475
|
+
(prisma.flowDefinition.findUnique as jest.Mock).mockResolvedValue({
|
|
476
|
+
workspaceId: WORKSPACE_ID,
|
|
477
|
+
});
|
|
478
|
+
(prisma.asset.findFirst as jest.Mock).mockResolvedValue(null);
|
|
479
|
+
(prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue(null);
|
|
480
|
+
|
|
481
|
+
await fulfillDeliverable(baseInput());
|
|
482
|
+
|
|
483
|
+
expect(prisma.asset.findFirst).toHaveBeenCalledWith({
|
|
484
|
+
where: { workspaceId: WORKSPACE_ID, key: "guide-small" },
|
|
485
|
+
select: { id: true },
|
|
486
|
+
});
|
|
487
|
+
expect(prisma.asset.findFirst).not.toHaveBeenCalledWith(
|
|
488
|
+
expect.objectContaining({
|
|
489
|
+
where: expect.objectContaining({ workspaceId: OTHER_WORKSPACE_ID }),
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
299
494
|
// ── D5 — grant mint ───────────────────────────────────────────────────────
|
|
300
495
|
|
|
301
496
|
it("mints the grant with assetId/leadId/sessionId", async () => {
|
|
@@ -521,3 +716,103 @@ describe("fulfillDeliverable", () => {
|
|
|
521
716
|
);
|
|
522
717
|
});
|
|
523
718
|
});
|
|
719
|
+
|
|
720
|
+
// ─── #6257 — the fence: every fenced read carries workspaceId ───────────────
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Unit C2's contract for this service (#6257 AC-1/AC-2). Three of its reads
|
|
724
|
+
* are workspace-scopable and are scoped here; the rest are keyed on the grant
|
|
725
|
+
* id itself — the capability token — and carry a global-key marker comment
|
|
726
|
+
* at the call site instead.
|
|
727
|
+
*
|
|
728
|
+
* `fulfillDeliverable`'s `siteId` is the session's own, resolved before it
|
|
729
|
+
* reaches this service (#6254/#6374 AC-7); `listSendlessGrants` takes an
|
|
730
|
+
* OPERATOR-SUPPLIED alias off the `deliverable:resend` CLI, so it resolves
|
|
731
|
+
* that alias itself and fails closed when it names no workspace.
|
|
732
|
+
*/
|
|
733
|
+
describe("#6257 — every fenced read carries workspaceId", () => {
|
|
734
|
+
const originalEnv = process.env;
|
|
735
|
+
|
|
736
|
+
beforeEach(() => {
|
|
737
|
+
jest.clearAllMocks();
|
|
738
|
+
process.env = {
|
|
739
|
+
...originalEnv,
|
|
740
|
+
AILK_PUBLIC_API_URL: "https://api.example.com",
|
|
741
|
+
AILK_DELIVERABLE_EMAIL_FROM: "deliverable-default@example.com",
|
|
742
|
+
};
|
|
743
|
+
mockSendDeliverableEmail.mockResolvedValue({ ok: true, messageId: "msg-test" });
|
|
744
|
+
(prisma.deliverableAsset.findUnique as jest.Mock).mockResolvedValue({
|
|
745
|
+
id: "dasset_00000000-0000-7000-8000-0000000000a1",
|
|
746
|
+
siteId: SITE_ID,
|
|
747
|
+
key: "guide-small",
|
|
748
|
+
});
|
|
749
|
+
(prisma.deliverableGrant.create as jest.Mock).mockResolvedValue({ id: GRANT_ID });
|
|
750
|
+
(prisma.deliverableGrant.update as jest.Mock).mockResolvedValue({ id: GRANT_ID });
|
|
751
|
+
(prisma.deliverableGrant.findMany as jest.Mock).mockResolvedValue([]);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
afterEach(() => {
|
|
755
|
+
process.env = originalEnv;
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it("scopes the LEGACY DeliverableAsset lookup by the session's workspace id", async () => {
|
|
759
|
+
await fulfillDeliverable(baseInput());
|
|
760
|
+
|
|
761
|
+
const { where } = (prisma.deliverableAsset.findUnique as jest.Mock).mock.calls[0][0];
|
|
762
|
+
expect(where.workspaceId).toBe(SITE_ID);
|
|
763
|
+
expect(where.siteId_key).toEqual({ siteId: SITE_ID, key: "guide-small" });
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("scopes the PlaybookInstance existence check by the session's workspace id", async () => {
|
|
767
|
+
(prisma.playbookInstance.findFirst as jest.Mock).mockResolvedValue({ id: "pbk_1" });
|
|
768
|
+
|
|
769
|
+
await fulfillDeliverable(
|
|
770
|
+
baseInput({
|
|
771
|
+
segment: undefined,
|
|
772
|
+
configSnapshot: {
|
|
773
|
+
slug: "get-a-quote",
|
|
774
|
+
defaultLocale: "en",
|
|
775
|
+
steps: [],
|
|
776
|
+
deliverables: {
|
|
777
|
+
assets: [{ assetKey: "playbook:targeting-maturity" }],
|
|
778
|
+
copy: { subject: en("x"), body: en("y") },
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
}),
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
expect(
|
|
785
|
+
(prisma.playbookInstance.findFirst as jest.Mock).mock.calls[0][0].where.workspaceId,
|
|
786
|
+
).toBe(SITE_ID);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("listSendlessGrants resolves the CLI alias and scopes the list by the RESOLVED workspace id", async () => {
|
|
790
|
+
// The `"default"` alias resolves to the seeded sentinel workspace — the
|
|
791
|
+
// list must be scoped by THAT id, never by the alias string the operator
|
|
792
|
+
// typed on the command line. Passing the alias and asserting the SENTINEL
|
|
793
|
+
// id back is what discriminates a resolution from a passthrough: a test
|
|
794
|
+
// that passed the workspace id as the alias would assert the same string
|
|
795
|
+
// either way and could not fail (security review, round 1).
|
|
796
|
+
(prisma.workspace.findUnique as jest.Mock).mockImplementation(
|
|
797
|
+
async ({ where }: { where: { id: string } }) =>
|
|
798
|
+
where.id === SENTINEL_WORKSPACE_ID
|
|
799
|
+
? { id: SENTINEL_WORKSPACE_ID, organizationId: SENTINEL_ORGANIZATION_ID }
|
|
800
|
+
: null,
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
await listSendlessGrants({ siteId: "default" });
|
|
804
|
+
|
|
805
|
+
const { where } = (prisma.deliverableGrant.findMany as jest.Mock).mock.calls[0][0];
|
|
806
|
+
expect(where.workspaceId).toBe(SENTINEL_WORKSPACE_ID);
|
|
807
|
+
expect(where.siteId).toBe(SENTINEL_WORKSPACE_ID);
|
|
808
|
+
expect(where.workspaceId).not.toBe("default");
|
|
809
|
+
expect(where.sendStatus).toBeNull();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it("listSendlessGrants fails closed on an alias that resolves to no workspace", async () => {
|
|
813
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
|
|
814
|
+
|
|
815
|
+
await expect(listSendlessGrants({ siteId: "no-such-site" })).resolves.toEqual([]);
|
|
816
|
+
expect(prisma.deliverableGrant.findMany).not.toHaveBeenCalled();
|
|
817
|
+
});
|
|
818
|
+
});
|
|
@@ -34,6 +34,10 @@ jest.mock("@working-theory/observability", () => ({
|
|
|
34
34
|
},
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
+
import {
|
|
38
|
+
mockNoWorkspace,
|
|
39
|
+
mockWorkspaceExists,
|
|
40
|
+
} from "../../lib/__mocks__/prisma.js";
|
|
37
41
|
import { prisma } from "../../lib/prisma.js";
|
|
38
42
|
import {
|
|
39
43
|
listSendlessGrants,
|
|
@@ -294,9 +298,25 @@ describe("resendDeliverableGrant", () => {
|
|
|
294
298
|
describe("listSendlessGrants", () => {
|
|
295
299
|
beforeEach(() => {
|
|
296
300
|
jest.clearAllMocks();
|
|
301
|
+
// #6335 — the shared prisma mock no longer resolves a workspace for any
|
|
302
|
+
// id asked of it, so this describe states the one it expects.
|
|
303
|
+
// `listSendlessGrants` resolves its `--site` alias before it reads
|
|
304
|
+
// (#6257); without this the resolution returns null, the function
|
|
305
|
+
// returns [] early, and every assertion below would be vacuous.
|
|
306
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
297
307
|
(prisma.deliverableGrant.findMany as jest.Mock).mockResolvedValue([]);
|
|
298
308
|
});
|
|
299
309
|
|
|
310
|
+
it("returns nothing and reads nothing when the site alias resolves to no workspace", async () => {
|
|
311
|
+
// The rejection path, so the explicit setup above is proven load-bearing
|
|
312
|
+
// rather than decorative (#6335 AC-6).
|
|
313
|
+
mockNoWorkspace(prisma);
|
|
314
|
+
|
|
315
|
+
await expect(listSendlessGrants({ siteId: SITE_ID })).resolves.toEqual([]);
|
|
316
|
+
|
|
317
|
+
expect(prisma.deliverableGrant.findMany).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
300
320
|
// ── D4 — list mode selects on sendStatus: null + siteId, no send ────────
|
|
301
321
|
|
|
302
322
|
it("selects on siteId + sendStatus: null and performs no send", async () => {
|